Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ playground/

# PNPM
.pnpm-store/
.pnpmfile.cjs

# MacOS
.DS_Store
.DS_Store
60 changes: 34 additions & 26 deletions cli/src/api/cloud/validate-cloud-link-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createAccountsHubClient, OBJECT_ID_REGEX } from '@powersync/cli-core';
import type { ResolvedCloudCLIConfig } from '@powersync/cli-schemas';

import { createAccountsHubClient, OBJECT_ID_REGEX, resolveCloudInstanceLink } from '@powersync/cli-core';
import { PowerSyncManagementClient } from '@powersync/management-client';

type InstanceConfigResponse = Awaited<ReturnType<PowerSyncManagementClient['getInstanceConfig']>>;

export type CloudLinkValidationInput = {
instanceId?: string;
orgId: string;
projectId: string;
orgId?: string;
projectId?: string;
};

export type ValidateCloudLinkConfigOptions = {
Expand All @@ -17,9 +19,14 @@ export type ValidateCloudLinkConfigOptions = {

export type ValidateCloudLinkConfigResult = {
instanceConfig?: InstanceConfigResponse;
linked?: ResolvedCloudCLIConfig;
};

function ensureObjectId(value: string, flagName: '--instance-id' | '--org-id' | '--project-id') {
function ensureObjectId(value: string | undefined, flagName: '--instance-id' | '--org-id' | '--project-id') {
if (value == null) {
return;
}

if (!OBJECT_ID_REGEX.test(value)) {
throw new Error(`Invalid ${flagName} "${value}". Expected a BSON ObjectID (24 hex characters).`);
}
Expand All @@ -31,6 +38,28 @@ export async function validateCloudLinkConfig(
const { cloudClient, input, validateInstance = false } = options;
const { instanceId, orgId, projectId } = input;

if (validateInstance) {
const linked = await resolveCloudInstanceLink({ client: cloudClient, instanceId, orgId, projectId });
let instanceConfig: InstanceConfigResponse;
try {
instanceConfig = await cloudClient.getInstanceConfig({
app_id: linked.project_id,
id: linked.instance_id,
org_id: linked.org_id
});
} catch {
throw new Error(
`Instance ${linked.instance_id} was not found in project ${linked.project_id} in organization ${linked.org_id}, or is not accessible with the current token.`
);
}

return { instanceConfig, linked };
}

if (!orgId || !projectId) {
throw new Error('Project validation requires both an organization ID and a project ID.');
}

ensureObjectId(orgId, '--org-id');
ensureObjectId(projectId, '--project-id');

Expand All @@ -57,26 +86,5 @@ export async function validateCloudLinkConfig(
);
}

if (!validateInstance) {
return {};
}

if (!instanceId) {
throw new Error('Instance validation requested but no instance ID was provided.');
}

ensureObjectId(instanceId, '--instance-id');

try {
const instanceConfig = await cloudClient.getInstanceConfig({
app_id: projectId,
id: instanceId,
org_id: orgId
});
return { instanceConfig };
} catch {
throw new Error(
`Instance ${instanceId} was not found in project ${projectId} in organization ${orgId}, or is not accessible with the current token.`
);
}
return {};
}
5 changes: 1 addition & 4 deletions cli/src/commands/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ export default class DeployAll extends WithSyncConfigFilePath(BaseDeployCommand)
`See also ${ux.colorize('blue', 'powersync deploy sync-config')} to deploy only sync config changes.`,
`See also ${ux.colorize('blue', 'powersync deploy service-config')} to deploy only service config changes.`
].join('\n');
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --instance-id=<id>'];
static flags = {
...GENERAL_VALIDATION_FLAG_HELPERS.flags
};
Expand Down
5 changes: 1 addition & 4 deletions cli/src/commands/deploy/service-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ const SERVICE_CONFIG_VALIDATION_FLAGS = generateValidationTestFlags({

export default class DeployServiceConfig extends BaseDeployCommand {
static description = 'Deploy only service config changes (without sync config updates).';
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --instance-id=<id>'];
static flags = {
...SERVICE_CONFIG_VALIDATION_FLAGS.flags
};
Expand Down
5 changes: 1 addition & 4 deletions cli/src/commands/deploy/sync-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ const SYNC_CONFIG_VALIDATION_FLAGS = generateValidationTestFlags({

export default class DeploySyncConfig extends WithSyncConfigFilePath(BaseDeployCommand) {
static description = 'Deploy only sync config changes.';
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --instance-id=<id>'];
static flags = {
...SYNC_CONFIG_VALIDATION_FLAGS.flags
};
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/fetch/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class FetchStatus extends SharedInstanceCommand {
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --output=json',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
'<%= config.bin %> <%= command.id %> --instance-id=<id>'
];
static flags = {
output: Flags.string({
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/generate/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class GenerateSchema extends WithSyncConfigFilePath(SharedInstanc
'Generate a client-side schema file from the instance database schema and sync config. Supports multiple output types (e.g. type, dart). Requires a linked instance. Cloud and self-hosted.';
static examples = [
'<%= config.bin %> <%= command.id %> --output=ts --output-path=schema.ts',
'<%= config.bin %> <%= command.id %> --output=dart --output-path=lib/schema.dart --instance-id=<id> --project-id=<id>'
'<%= config.bin %> <%= command.id %> --output=dart --output-path=lib/schema.dart --instance-id=<id>'
];
static flags = {
output: Flags.string({
Expand Down
5 changes: 1 addition & 4 deletions cli/src/commands/init/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ export default class InitCloud extends InstanceCommand {
'Create a new instance with ',
ux.colorize('blue', '\tpowersync link cloud --create --org-id=<org-id> --project-id=<project-id>'),
'or pull an existing instance with ',
ux.colorize(
'blue',
'\tpowersync pull instance --org-id=<org-id> --project-id=<project-id> --instance-id=<instance-id>'
),
ux.colorize('blue', '\tpowersync pull instance --instance-id=<instance-id>'),
`Tip: use ${ux.colorize('blue', 'powersync fetch instances')} to see available organizations and projects for your token.`,
'Then run',
ux.colorize('blue', '\tpowersync deploy'),
Expand Down
51 changes: 36 additions & 15 deletions cli/src/commands/link/cloud.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ResolvedCloudCLIConfig } from '@powersync/cli-schemas';

import { Flags, ux } from '@oclif/core';
import {
CLI_FILENAME,
Expand All @@ -16,9 +18,9 @@ import { writeCloudLink } from '../../api/cloud/write-cloud-link.js';
export default class LinkCloud extends CloudInstanceCommand {
static commandHelpGroup = CommandHelpGroup.PROJECT_SETUP;
static description =
'Write or update cli.yaml with a Cloud instance (instance-id, org-id, project-id). Use --create to create a new instance from service.yaml name/region and link it; omit --instance-id when using --create. Org ID is optional when the token has a single organization.';
'Write or update cli.yaml with a Cloud instance. Use --create to create a new instance from service.yaml name/region and link it; omit --instance-id when using --create.';
static examples = [
'<%= config.bin %> <%= command.id %> --project-id=<project-id>',
'<%= config.bin %> <%= command.id %> --instance-id=<id>',
'<%= config.bin %> <%= command.id %> --create --project-id=<project-id>',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<project-id> --org-id=<org-id>'
];
Expand All @@ -36,13 +38,13 @@ export default class LinkCloud extends CloudInstanceCommand {
'org-id': Flags.string({
default: env.ORG_ID,
description:
'Organization ID. Optional when the token has a single org; required when the token has multiple orgs. Resolved: flag → ORG_ID → cli.yaml.',
'Organization ID. Required with --create when the token has multiple orgs; optional when linking an existing instance.',
required: false
}),
'project-id': Flags.string({
default: env.PROJECT_ID,
description: 'Project ID. Resolved: flag → PROJECT_ID → cli.yaml.',
required: true
description: 'Project ID. Required with --create; optional assertion when linking an existing instance.',
required: false
})
};
static summary = '[Cloud only] Link to a PowerSync Cloud instance (or create one with --create).';
Expand All @@ -51,10 +53,6 @@ export default class LinkCloud extends CloudInstanceCommand {
const { flags } = await this.parse(LinkCloud);
let { create, directory, 'instance-id': instanceId, 'org-id': orgId, 'project-id': projectId } = flags;

if (!orgId) {
orgId = await getDefaultOrgId();
}

const projectDirectory = this.resolveProjectDir(flags);
if (create) {
if (instanceId) {
Expand All @@ -63,10 +61,23 @@ export default class LinkCloud extends CloudInstanceCommand {
});
}

if (!projectId) {
this.styledError({
message: 'Creating a Cloud instance requires --project-id.'
});
}

if (!orgId) {
orgId = await getDefaultOrgId();
}

const createOrgId = orgId!;
const createProjectId = projectId!;

try {
await validateCloudLinkConfig({
cloudClient: this.client,
input: { orgId, projectId },
input: { orgId: createOrgId, projectId: createProjectId },
validateInstance: false
});
} catch (error) {
Expand All @@ -80,8 +91,8 @@ export default class LinkCloud extends CloudInstanceCommand {
try {
const result = await createCloudInstance(client, {
name: config.name,
orgId,
projectId,
orgId: createOrgId,
projectId: createProjectId,
region: config.region
});
newInstanceId = result.instanceId;
Expand All @@ -96,7 +107,7 @@ export default class LinkCloud extends CloudInstanceCommand {
expectedType: ServiceType.CLOUD,
projectDir: projectDirectory
});
writeCloudLink(projectDirectory, { instanceId: newInstanceId, orgId, projectId });
writeCloudLink(projectDirectory, { instanceId: newInstanceId, orgId: createOrgId, projectId: createProjectId });
this.log(
ux.colorize('green', `Created Cloud instance ${newInstanceId} and updated ${directory}/${CLI_FILENAME}.`)
);
Expand All @@ -110,17 +121,27 @@ export default class LinkCloud extends CloudInstanceCommand {
});
}

let linked: ResolvedCloudCLIConfig | undefined;
try {
await validateCloudLinkConfig({
const validationResult = await validateCloudLinkConfig({
cloudClient: this.client,
input: { instanceId, orgId, projectId },
validateInstance: true
});
linked = validationResult.linked;
} catch (error) {
this.styledError({ message: error instanceof Error ? error.message : String(error) });
}

writeCloudLink(projectDirectory, { instanceId, orgId, projectId });
if (!linked) {
this.styledError({ message: `Failed to resolve Cloud instance ${instanceId}.` });
}

writeCloudLink(projectDirectory, {
instanceId: linked.instance_id,
orgId: linked.org_id,
projectId: linked.project_id
});
ensureServiceTypeMatches({
command: this,
configRequired: false,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/pull/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from '@oclif/core';

export default class Pull extends Command {
static description =
'Download current config from PowerSync Cloud into local YAML files. Use pull instance; pass --instance-id and --project-id when the directory is not yet linked (--org-id is optional when the token has a single organization).';
'Download current config from PowerSync Cloud into local YAML files. Use pull instance; pass --instance-id when the directory is not yet linked.';
static examples = ['<%= config.bin %> <%= command.id %>'];
static hidden = true;
static summary = '[Cloud only] Download Cloud config into local service.yaml and sync-config.yaml.';
Expand Down
Loading
Loading