Skip to content

Commit 16352ad

Browse files
unify config types
1 parent f8021ca commit 16352ad

File tree

18 files changed

+201
-74
lines changed

18 files changed

+201
-74
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ensureServiceTypeMatches } from '../utils/ensureServiceType.js';
2+
import { InstanceCommand } from './InstanceCommand.js';
3+
4+
/** Base command for operations that require a Cloud-type PowerSync project (service.yaml _type: cloud). */
5+
export abstract class CloudInstanceCommand extends InstanceCommand {
6+
static flags = {
7+
...InstanceCommand.flags
8+
};
9+
10+
/**
11+
* Ensures the project directory exists and service.yaml has _type: cloud.
12+
* @returns The resolved absolute path to the project directory.
13+
*/
14+
ensureConfigType(directory: string): string {
15+
const projectDir = this.ensureProjectDirExists(directory);
16+
ensureServiceTypeMatches(this, projectDir, 'cloud', directory);
17+
return projectDir;
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ensureServiceTypeMatches } from '../utils/ensureServiceType.js';
2+
import { InstanceCommand } from './InstanceCommand.js';
3+
4+
/** Base command for operations that require a self-hosted PowerSync project (service.yaml _type: self-hosted). */
5+
export abstract class SelfHostedInstanceCommand extends InstanceCommand {
6+
static flags = {
7+
...InstanceCommand.flags
8+
};
9+
10+
/**
11+
* Ensures the project directory exists and service.yaml has _type: self-hosted.
12+
* @returns The resolved absolute path to the project directory.
13+
*/
14+
ensureConfigType(directory: string): string {
15+
const projectDir = this.ensureProjectDirExists(directory);
16+
ensureServiceTypeMatches(this, projectDir, 'self-hosted', directory);
17+
return projectDir;
18+
}
19+
}

packages/cli/src/commands/link/cloud.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { Flags } from '@oclif/core';
22
import { writeFileSync } from 'node:fs';
33
import { join } from 'node:path';
44

5-
import { InstanceCommand } from '../../command-types/InstanceCommand.js';
5+
import { CloudInstanceCommand } from '../../command-types/CloudInstanceCommand.js';
66
import { loadLinkDocument } from '../../utils/loadLinkDoc.js';
77

88
const LINK_FILENAME = 'link.yaml';
99

10-
export default class LinkCloud extends InstanceCommand {
10+
export default class LinkCloud extends CloudInstanceCommand {
1111
static description = 'Link this directory to a PowerSync Cloud instance.';
1212
static summary = 'Link to PowerSync Cloud (instance ID, org, project).';
1313
static flags = {
14-
...InstanceCommand.flags,
14+
...CloudInstanceCommand.flags,
1515
/**
1616
* TODO, we could default these to the values used after login
1717
*/
@@ -33,7 +33,7 @@ export default class LinkCloud extends InstanceCommand {
3333
const { flags } = await this.parse(LinkCloud);
3434
const { directory, 'instance-id': instanceId, 'org-id': orgId, 'project-id': projectId } = flags;
3535

36-
const projectDir = this.ensureProjectDirExists(directory);
36+
const projectDir = this.ensureConfigType(directory);
3737

3838
const linkPath = join(projectDir, LINK_FILENAME);
3939
const doc = loadLinkDocument(linkPath);

packages/cli/src/commands/link/self-hosted.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { Flags } from '@oclif/core';
22
import { writeFileSync } from 'node:fs';
33
import { join } from 'node:path';
44

5-
import { InstanceCommand } from '../../command-types/InstanceCommand.js';
5+
import { SelfHostedInstanceCommand } from '../../command-types/SelfHostedInstanceCommand.js';
66
import { loadLinkDocument } from '../../utils/loadLinkDoc.js';
77

88
const LINK_FILENAME = 'link.yaml';
99

10-
export default class LinkSelfHosted extends InstanceCommand {
10+
export default class LinkSelfHosted extends SelfHostedInstanceCommand {
1111
static description = 'Link this directory to a self-hosted PowerSync instance.';
1212
static summary = 'Link to self-hosted PowerSync (API URL and token).';
1313
static flags = {
14-
...InstanceCommand.flags,
14+
...SelfHostedInstanceCommand.flags,
1515
url: Flags.string({
1616
description: 'Self-hosted PowerSync API base URL (e.g. https://powersync.example.com).',
1717
required: true
@@ -26,7 +26,7 @@ export default class LinkSelfHosted extends InstanceCommand {
2626
const { flags } = await this.parse(LinkSelfHosted);
2727
const { directory, url, 'api-key': apiKey } = flags;
2828

29-
const projectDir = this.ensureProjectDirExists(directory);
29+
const projectDir = this.ensureConfigType(directory);
3030

3131
const linkPath = join(projectDir, LINK_FILENAME);
3232
const doc = loadLinkDocument(linkPath);
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {Command} from '@oclif/core'
1+
import { SelfHostedInstanceCommand } from '../command-types/SelfHostedInstanceCommand.js';
22

3-
export default class Migrate extends Command {
4-
static description = 'Migrates a self-hosted instance configuration to PowerSync Cloud format. Self-hosted only.'
5-
static summary = 'Migrate a self-hosted config to a cloud config.'
3+
export default class Migrate extends SelfHostedInstanceCommand {
4+
static description = 'Migrates a self-hosted instance configuration to PowerSync Cloud format. Self-hosted only.';
5+
static summary = 'Migrate a self-hosted config to a cloud config.';
66

77
async run(): Promise<void> {
8-
this.log('migrate: not yet implemented')
8+
this.log('migrate: not yet implemented');
99
}
1010
}

packages/cli/src/commands/stop.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {Command} from '@oclif/core'
1+
import { Command } from '@oclif/core';
22

33
export default class Stop extends Command {
4-
static description = 'Stops the linked PowerSync Cloud instance. Cloud only.'
5-
static summary = 'Stop a PowerSync instance.'
4+
static description = 'Stops the linked PowerSync Cloud instance. Cloud only.';
5+
static summary = 'Stop a PowerSync instance.';
66

77
async run(): Promise<void> {
8-
this.log('stop: not yet implemented')
8+
this.log('stop: not yet implemented');
99
}
1010
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { parse as parseYaml } from 'yaml';
4+
5+
const SERVICE_FILENAME = 'service.yaml';
6+
7+
export type ServiceType = 'cloud' | 'self-hosted';
8+
9+
/**
10+
* Ensures service.yaml exists in the project dir and its _type matches the expected link type.
11+
* Calls command.error and exits if the file is missing, or _type is missing or mismatched.
12+
*/
13+
export function ensureServiceTypeMatches(
14+
command: { error: (message: string, options: { exit: number }) => never },
15+
projectDir: string,
16+
expectedType: ServiceType,
17+
directoryLabel: string
18+
): void {
19+
const servicePath = join(projectDir, SERVICE_FILENAME);
20+
21+
if (!existsSync(servicePath)) {
22+
command.error(
23+
`No ${SERVICE_FILENAME} found in "${directoryLabel}". Run \`powersync init\` first to create the project.`,
24+
{ exit: 1 }
25+
);
26+
}
27+
28+
const content = readFileSync(servicePath, 'utf8');
29+
const service = parseYaml(content) as { _type?: string };
30+
31+
if (service._type === undefined || service._type === null) {
32+
command.error(
33+
`${SERVICE_FILENAME} in "${directoryLabel}" is missing \`_type\`. Add \`_type: ${expectedType}\` to match this link command.`,
34+
{ exit: 1 }
35+
);
36+
}
37+
38+
if (service._type !== expectedType) {
39+
command.error(
40+
`${SERVICE_FILENAME} in "${directoryLabel}" has \`_type: ${service._type}\` but you are running \`link ${expectedType}\`. The _type must match. Use \`powersync init --type=${expectedType}\` to create a project of the correct type, or change _type in ${SERVICE_FILENAME}.`,
41+
{ exit: 1 }
42+
);
43+
}
44+
}

packages/cli/templates/cloud/powersync/service.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# TODO update this with published schema
2-
# yaml-language-server: $schema=/Users/stevenontong/Documents/platform_code/powersync/service/powersync-hosted/public-packages/types/json-schema/powersync-config.json
2+
# yaml-language-server: $schema=/Users/stevenontong/Documents/platform_code/powersync/new-cli/packages/schemas/json-schema/cli-config.json
3+
# yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json
34
#
45
# PowerSync Cloud config – example template with all schema options documented.
56
# Uncomment one block per type (one connection type + optional client_auth). We recommend secrets as: secret: !env ENV_NAME (see ../../.vscode/settings.json).
@@ -9,6 +10,8 @@
910
# TOP LEVEL
1011
# -----------------------------------------------------------------------------
1112

13+
_type: cloud
14+
1215
# region (required): deployment region, e.g. us
1316
# Note: This cannot be changed after the initial deployment
1417
region: us

packages/cli/templates/self-hosted/base/powersync/service.yaml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json
1+
# TODO update this with published schema
2+
# yaml-language-server: $schema=/Users/stevenontong/Documents/platform_code/powersync/new-cli/packages/schemas/json-schema/cli-config.json
3+
# yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json
24
#
35
# PowerSync self-hosted config – example template with all schema options documented.
46
# Uncomment one block per type where applicable. We recommend secrets via !env ENV_NAME.
57
# Docs: https://docs.powersync.com/self-hosting
6-
#
8+
9+
_type: self-hosted
10+
711
# -----------------------------------------------------------------------------
812
# TELEMETRY – configuration for service telemetry and monitoring
913
# -----------------------------------------------------------------------------
1014
# See https://docs.powersync.com/self-hosting/telemetry
1115
telemetry:
12-
# When true, disables sharing of anonymized telemetry data
16+
# When true, disables sharing of anonymized telemetry data
1317
disable_telemetry_sharing: false
1418
# Port on which Prometheus metrics will be exposed. When set, metrics will be available on this port for scraping.
1519

@@ -129,7 +133,7 @@ port: !env PS_PORT
129133
# -----------------------------------------------------------------------------
130134
# One of path or content is supported. path is used in this example.
131135
sync_rules:
132-
# Path to the sync rules YAML file.
136+
# Path to the sync rules YAML file.
133137
path: sync_rules.yaml
134138
# Inline sync rules content as a string (use this or path, not both).
135139
# content: string
@@ -184,7 +188,7 @@ sync_rules:
184188
# API – API service configuration and parameters
185189
# -----------------------------------------------------------------------------
186190
api:
187-
# API access tokens for administrative operations.
191+
# API access tokens for administrative operations.
188192
tokens:
189193
- use_a_better_token_in_production
190194
# Performance and safety parameters for the API service.

packages/cli/test/commands/link.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import { root } from '../helpers/root.js';
99

1010
const LINK_FILENAME = 'link.yaml';
1111
const PROJECT_DIR = 'powersync';
12+
const SERVICE_FILENAME = 'service.yaml';
13+
14+
function writeServiceYaml(projectDir: string, type: 'cloud' | 'self-hosted') {
15+
writeFileSync(join(projectDir, SERVICE_FILENAME), `_type: ${type}\n`, 'utf8');
16+
}
1217

1318
describe('link', () => {
1419
describe('cloud', () => {
@@ -34,8 +39,28 @@ describe('link', () => {
3439
expect(result.error?.oclif?.exit).toBe(1);
3540
});
3641

37-
it('creates link.yaml with cloud config when directory exists', async () => {
42+
it('errors when service.yaml is missing', async () => {
3843
mkdirSync(join(tmpDir, PROJECT_DIR), { recursive: true });
44+
const result = await runCommand('link cloud --instance-id=inst --org-id=o --project-id=p', { root });
45+
expect(result.error?.message).toMatch(
46+
new RegExp(`No ${SERVICE_FILENAME} found in "${PROJECT_DIR}". Run \`powersync init\` first`)
47+
);
48+
expect(result.error?.oclif?.exit).toBe(1);
49+
});
50+
51+
it('errors when service.yaml _type does not match (self-hosted)', async () => {
52+
const projectDir = join(tmpDir, PROJECT_DIR);
53+
mkdirSync(projectDir, { recursive: true });
54+
writeServiceYaml(projectDir, 'self-hosted');
55+
const result = await runCommand('link cloud --instance-id=inst --org-id=o --project-id=p', { root });
56+
expect(result.error?.message).toMatch(/has `_type: self-hosted` but you are running `link cloud`/);
57+
expect(result.error?.oclif?.exit).toBe(1);
58+
});
59+
60+
it('creates link.yaml with cloud config when directory exists and service _type is cloud', async () => {
61+
const projectDir = join(tmpDir, PROJECT_DIR);
62+
mkdirSync(projectDir, { recursive: true });
63+
writeServiceYaml(projectDir, 'cloud');
3964
const { stdout } = await runCommand('link cloud --instance-id=inst-1 --org-id=org-1 --project-id=proj-1', {
4065
root
4166
});
@@ -52,6 +77,7 @@ describe('link', () => {
5277
it('updates existing link.yaml and preserves comments', async () => {
5378
const projectDir = join(tmpDir, PROJECT_DIR);
5479
mkdirSync(projectDir, { recursive: true });
80+
writeServiceYaml(projectDir, 'cloud');
5581
const linkPath = join(projectDir, LINK_FILENAME);
5682
const withComments = `# Managed by PowerSync CLI
5783
# Run powersync link --help for info
@@ -72,6 +98,7 @@ type: cloud
7298
it('respects --directory flag', async () => {
7399
const customDir = 'my-powersync';
74100
mkdirSync(join(tmpDir, customDir), { recursive: true });
101+
writeServiceYaml(join(tmpDir, customDir), 'cloud');
75102
const { stdout } = await runCommand(
76103
`link cloud --directory=${customDir} --instance-id=i --org-id=o --project-id=p`,
77104
{ root }
@@ -106,8 +133,28 @@ type: cloud
106133
expect(result.error?.oclif?.exit).toBe(1);
107134
});
108135

109-
it('creates link.yaml with self-hosted config when directory exists', async () => {
136+
it('errors when service.yaml is missing', async () => {
110137
mkdirSync(join(tmpDir, PROJECT_DIR), { recursive: true });
138+
const result = await runCommand('link self-hosted --url=https://x.com --api-key=k', { root });
139+
expect(result.error?.message).toMatch(
140+
new RegExp(`No ${SERVICE_FILENAME} found in "${PROJECT_DIR}". Run \`powersync init\` first`)
141+
);
142+
expect(result.error?.oclif?.exit).toBe(1);
143+
});
144+
145+
it('errors when service.yaml _type does not match (cloud)', async () => {
146+
const projectDir = join(tmpDir, PROJECT_DIR);
147+
mkdirSync(projectDir, { recursive: true });
148+
writeServiceYaml(projectDir, 'cloud');
149+
const result = await runCommand('link self-hosted --url=https://x.com --api-key=k', { root });
150+
expect(result.error?.message).toMatch(/has `_type: cloud` but you are running `link self-hosted`/);
151+
expect(result.error?.oclif?.exit).toBe(1);
152+
});
153+
154+
it('creates link.yaml with self-hosted config when directory exists and service _type is self-hosted', async () => {
155+
const projectDir = join(tmpDir, PROJECT_DIR);
156+
mkdirSync(projectDir, { recursive: true });
157+
writeServiceYaml(projectDir, 'self-hosted');
111158
const { stdout } = await runCommand('link self-hosted --url=https://sync.example.com --api-key=my-token', {
112159
root
113160
});
@@ -123,6 +170,7 @@ type: cloud
123170
it('updates existing link.yaml and preserves comments', async () => {
124171
const projectDir = join(tmpDir, PROJECT_DIR);
125172
mkdirSync(projectDir, { recursive: true });
173+
writeServiceYaml(projectDir, 'self-hosted');
126174
const linkPath = join(projectDir, LINK_FILENAME);
127175
const withComments = `# Self-hosted config
128176
type: self-hosted
@@ -140,6 +188,7 @@ type: self-hosted
140188
it('respects --directory flag', async () => {
141189
const customDir = 'my-powersync';
142190
mkdirSync(join(tmpDir, customDir), { recursive: true });
191+
writeServiceYaml(join(tmpDir, customDir), 'self-hosted');
143192
const { stdout } = await runCommand(
144193
`link self-hosted --directory=${customDir} --url=https://example.com --api-key=k`,
145194
{

0 commit comments

Comments
 (0)