Skip to content

Commit 002b85f

Browse files
committed
Split validateCloudLinkConfig and merge ensureObjectId duplicates
1 parent d62de13 commit 002b85f

5 files changed

Lines changed: 81 additions & 113 deletions

File tree

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,30 @@
11
import type { ResolvedCloudCLIConfig } from '@powersync/cli-schemas';
22

3-
import { createAccountsHubClient, OBJECT_ID_REGEX, resolveCloudInstanceLink } from '@powersync/cli-core';
3+
import { createAccountsHubClient, ensureObjectId, resolveCloudInstanceLink } from '@powersync/cli-core';
44
import { PowerSyncManagementClient } from '@powersync/management-client';
55

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

8-
export type CloudLinkValidationInput = {
9-
instanceId?: string;
10-
orgId?: string;
11-
projectId?: string;
8+
export type ValidateCloudProjectOptions = {
9+
cloudClient: PowerSyncManagementClient;
10+
orgId: string;
11+
projectId: string;
1212
};
1313

14-
export type ValidateCloudLinkConfigOptions = {
14+
export type FetchCloudInstanceConfigOptions = {
1515
cloudClient: PowerSyncManagementClient;
16-
input: CloudLinkValidationInput;
17-
validateInstance?: boolean;
16+
instanceId?: string;
17+
orgId?: string;
18+
projectId?: string;
1819
};
1920

20-
export type ValidateCloudLinkConfigResult = {
21-
instanceConfig?: InstanceConfigResponse;
22-
linked?: ResolvedCloudCLIConfig;
21+
export type FetchCloudInstanceConfigResult = {
22+
instanceConfig: InstanceConfigResponse;
23+
linked: ResolvedCloudCLIConfig;
2324
};
2425

25-
function ensureObjectId(value: string | undefined, flagName: '--instance-id' | '--org-id' | '--project-id') {
26-
if (value == null) {
27-
return;
28-
}
29-
30-
if (!OBJECT_ID_REGEX.test(value)) {
31-
throw new Error(`Invalid ${flagName} "${value}". Expected a BSON ObjectID (24 hex characters).`);
32-
}
33-
}
34-
35-
export async function validateCloudLinkConfig(
36-
options: ValidateCloudLinkConfigOptions
37-
): Promise<ValidateCloudLinkConfigResult> {
38-
const { cloudClient, input, validateInstance = false } = options;
39-
const { instanceId, orgId, projectId } = input;
40-
41-
if (validateInstance) {
42-
const linked = await resolveCloudInstanceLink({ client: cloudClient, instanceId, orgId, projectId });
43-
let instanceConfig: InstanceConfigResponse;
44-
try {
45-
instanceConfig = await cloudClient.getInstanceConfig({
46-
app_id: linked.project_id,
47-
id: linked.instance_id,
48-
org_id: linked.org_id
49-
});
50-
} catch {
51-
throw new Error(
52-
`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.`
53-
);
54-
}
55-
56-
return { instanceConfig, linked };
57-
}
58-
59-
if (!orgId || !projectId) {
60-
throw new Error('Project validation requires both an organization ID and a project ID.');
61-
}
26+
export async function validateCloudProject(options: ValidateCloudProjectOptions): Promise<void> {
27+
const { orgId, projectId } = options;
6228

6329
ensureObjectId(orgId, '--org-id');
6430
ensureObjectId(projectId, '--project-id');
@@ -85,6 +51,27 @@ export async function validateCloudLinkConfig(
8551
`Project ${projectId} was not found in organization ${orgId}, or is not accessible with the current token.`
8652
);
8753
}
54+
}
55+
56+
export async function fetchCloudInstanceConfig(
57+
options: FetchCloudInstanceConfigOptions
58+
): Promise<FetchCloudInstanceConfigResult> {
59+
const { cloudClient, instanceId, orgId, projectId } = options;
60+
61+
const linked = await resolveCloudInstanceLink({ client: cloudClient, instanceId, orgId, projectId });
62+
63+
let instanceConfig: InstanceConfigResponse;
64+
try {
65+
instanceConfig = await cloudClient.getInstanceConfig({
66+
app_id: linked.project_id,
67+
id: linked.instance_id,
68+
org_id: linked.org_id
69+
});
70+
} catch {
71+
throw new Error(
72+
`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.`
73+
);
74+
}
8875

89-
return {};
76+
return { instanceConfig, linked };
9077
}

cli/src/commands/link/cloud.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@powersync/cli-core';
1313

1414
import { createCloudInstance } from '../../api/cloud/create-cloud-instance.js';
15-
import { validateCloudLinkConfig } from '../../api/cloud/validate-cloud-link-config.js';
15+
import { fetchCloudInstanceConfig, validateCloudProject } from '../../api/cloud/validate-cloud-link-config.js';
1616
import { writeCloudLink } from '../../api/cloud/write-cloud-link.js';
1717

1818
export default class LinkCloud extends CloudInstanceCommand {
@@ -71,15 +71,8 @@ export default class LinkCloud extends CloudInstanceCommand {
7171
orgId = await getDefaultOrgId();
7272
}
7373

74-
const createOrgId = orgId!;
75-
const createProjectId = projectId!;
76-
7774
try {
78-
await validateCloudLinkConfig({
79-
cloudClient: this.client,
80-
input: { orgId: createOrgId, projectId: createProjectId },
81-
validateInstance: false
82-
});
75+
await validateCloudProject({ cloudClient: this.client, orgId: orgId!, projectId: projectId! });
8376
} catch (error) {
8477
this.styledError({ message: error instanceof Error ? error.message : String(error) });
8578
}
@@ -91,8 +84,8 @@ export default class LinkCloud extends CloudInstanceCommand {
9184
try {
9285
const result = await createCloudInstance(client, {
9386
name: config.name,
94-
orgId: createOrgId,
95-
projectId: createProjectId,
87+
orgId: orgId!,
88+
projectId: projectId!,
9689
region: config.region
9790
});
9891
newInstanceId = result.instanceId;
@@ -107,7 +100,7 @@ export default class LinkCloud extends CloudInstanceCommand {
107100
expectedType: ServiceType.CLOUD,
108101
projectDir: projectDirectory
109102
});
110-
writeCloudLink(projectDirectory, { instanceId: newInstanceId, orgId: createOrgId, projectId: createProjectId });
103+
writeCloudLink(projectDirectory, { instanceId: newInstanceId, orgId: orgId!, projectId: projectId! });
111104
this.log(
112105
ux.colorize('green', `Created Cloud instance ${newInstanceId} and updated ${directory}/${CLI_FILENAME}.`)
113106
);
@@ -123,10 +116,11 @@ export default class LinkCloud extends CloudInstanceCommand {
123116

124117
let linked: ResolvedCloudCLIConfig | undefined;
125118
try {
126-
const validationResult = await validateCloudLinkConfig({
119+
const validationResult = await fetchCloudInstanceConfig({
127120
cloudClient: this.client,
128-
input: { instanceId, orgId, projectId },
129-
validateInstance: true
121+
instanceId,
122+
orgId,
123+
projectId
130124
});
131125
linked = validationResult.linked;
132126
} catch (error) {

cli/src/commands/pull/instance.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { join } from 'node:path';
1919
import { buildServiceYaml } from '../../api/build-service-yaml.js';
2020
import { CLOUD_SERVICE_TEMPLATE_PATH, writeCloudSyncConfigFile } from '../../api/cloud/create-cloud-template.js';
2121
import { decodeFetchedCloudConfig } from '../../api/cloud/fetch-cloud-config.js';
22-
import { validateCloudLinkConfig } from '../../api/cloud/validate-cloud-link-config.js';
22+
import { fetchCloudInstanceConfig } from '../../api/cloud/validate-cloud-link-config.js';
2323
import { writeCloudLink } from '../../api/cloud/write-cloud-link.js';
2424

2525
const SERVICE_FETCHED_FILENAME = 'service-fetched.yaml';
@@ -56,9 +56,6 @@ export default class PullInstance extends CloudInstanceCommand {
5656

5757
let resolvedLink: ResolvedCloudCLIConfig | undefined;
5858
let instanceConfig;
59-
/**
60-
* The pull instance command can be used to create a new powersync project directory
61-
*/
6259
const projectDir = this.resolveProjectDir(flags);
6360
if (!existsSync(projectDir)) {
6461
if (!inputInstanceId) {
@@ -68,10 +65,11 @@ export default class PullInstance extends CloudInstanceCommand {
6865
}
6966

7067
try {
71-
const validationResult = await validateCloudLinkConfig({
68+
const validationResult = await fetchCloudInstanceConfig({
7269
cloudClient: this.client,
73-
input: { instanceId: inputInstanceId, orgId: inputOrgId, projectId: inputProjectId },
74-
validateInstance: true
70+
instanceId: inputInstanceId,
71+
orgId: inputOrgId,
72+
projectId: inputProjectId
7573
});
7674
resolvedLink = validationResult.linked;
7775
instanceConfig = validationResult.instanceConfig;
@@ -100,10 +98,11 @@ export default class PullInstance extends CloudInstanceCommand {
10098
}
10199

102100
try {
103-
const validationResult = await validateCloudLinkConfig({
101+
const validationResult = await fetchCloudInstanceConfig({
104102
cloudClient: this.client,
105-
input: { instanceId: inputInstanceId, orgId: inputOrgId, projectId: inputProjectId },
106-
validateInstance: true
103+
instanceId: inputInstanceId,
104+
orgId: inputOrgId,
105+
projectId: inputProjectId
107106
});
108107
resolvedLink = validationResult.linked;
109108
instanceConfig = validationResult.instanceConfig;
@@ -130,14 +129,11 @@ export default class PullInstance extends CloudInstanceCommand {
130129

131130
if (!instanceConfig) {
132131
try {
133-
const validationResult = await validateCloudLinkConfig({
132+
const validationResult = await fetchCloudInstanceConfig({
134133
cloudClient: this.client,
135-
input: {
136-
instanceId: linked.instance_id,
137-
orgId: linked.org_id,
138-
projectId: linked.project_id
139-
},
140-
validateInstance: true
134+
instanceId: linked.instance_id,
135+
orgId: linked.org_id,
136+
projectId: linked.project_id
141137
});
142138
instanceConfig = validationResult.instanceConfig;
143139
} catch (error) {
@@ -191,7 +187,6 @@ export default class PullInstance extends CloudInstanceCommand {
191187
writeFileSync(syncOutputPath, YAML_SYNC_RULES_SCHEMA + '\n' + fetched.syncRules, 'utf8');
192188
this.log(`Wrote ${ux.colorize('blue', syncOutputName)} with sync config from the cloud.`);
193189
} else if (!fetched.syncRules && !syncExists) {
194-
// If there is no sync config in the cloud and no existing sync config locally, we should still create an empty sync-config.yaml with the correct header and schema reference
195190
await writeCloudSyncConfigFile({ targetDir: projectDir });
196191
this.log(
197192
`Wrote ${ux.colorize('blue', SYNC_FILENAME)} with template sync config (no sync config found in the cloud).`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export const OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/;
2+
3+
export function ensureObjectId(value: string, label: string): void {
4+
if (!OBJECT_ID_REGEX.test(value)) {
5+
throw new Error(`Invalid ${label} "${value}". Expected a BSON ObjectID (24 hex characters).`);
6+
}
7+
}

packages/cli-core/src/utils/resolve-cloud-instance-link.ts

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import type { ResolvedCloudCLIConfig } from '@powersync/cli-schemas';
22
import type { PowerSyncManagementClient } from '@powersync/management-client';
33

4-
import { OBJECT_ID_REGEX } from './object-id.js';
5-
6-
type CloudInstanceMetadata = {
7-
app_id: string;
8-
id: string;
9-
org_id: string;
10-
};
11-
12-
type CloudInstanceResolverClient = PowerSyncManagementClient & {
13-
getInstance(input: { id: string }): Promise<CloudInstanceMetadata>;
14-
};
4+
import { ensureObjectId } from './object-id.js';
155

166
export type ResolveCloudInstanceLinkInput = {
177
client: PowerSyncManagementClient;
@@ -20,31 +10,21 @@ export type ResolveCloudInstanceLinkInput = {
2010
projectId?: string;
2111
};
2212

23-
function ensureObjectId(value: string | undefined, label: '--instance-id' | '--org-id' | '--project-id'): void {
24-
if (value == null) {
25-
return;
26-
}
27-
28-
if (!OBJECT_ID_REGEX.test(value)) {
29-
throw new Error(`Invalid ${label} "${value}". Expected a BSON ObjectID (24 hex characters).`);
30-
}
31-
}
32-
3313
/**
3414
* Resolves the full Cloud link from an instance ID. If org/project IDs are missing, fetches them from the instance.
3515
*/
3616
export async function resolveCloudInstanceLink(input: ResolveCloudInstanceLinkInput): Promise<ResolvedCloudCLIConfig> {
3717
const { client, instanceId, orgId, projectId } = input;
3818

39-
ensureObjectId(instanceId, '--instance-id');
40-
ensureObjectId(orgId, '--org-id');
41-
ensureObjectId(projectId, '--project-id');
42-
4319
if (!instanceId) {
4420
throw new Error('Cloud instance resolution requires an instance ID.');
4521
}
4622

23+
ensureObjectId(instanceId, '--instance-id');
24+
4725
if (orgId && projectId) {
26+
ensureObjectId(orgId, '--org-id');
27+
ensureObjectId(projectId, '--project-id');
4828
return {
4929
instance_id: instanceId,
5030
org_id: orgId,
@@ -53,19 +33,25 @@ export async function resolveCloudInstanceLink(input: ResolveCloudInstanceLinkIn
5333
};
5434
}
5535

56-
let instance: CloudInstanceMetadata;
36+
let instance;
5737
try {
58-
instance = await (client as CloudInstanceResolverClient).getInstance({ id: instanceId });
38+
instance = await client.getInstance({ id: instanceId });
5939
} catch {
6040
throw new Error(`Instance ${instanceId} was not found or is not accessible with the current token.`);
6141
}
6242

63-
if (orgId && orgId !== instance.org_id) {
64-
throw new Error(`Instance ${instanceId} belongs to organization ${instance.org_id}, not ${orgId}.`);
43+
if (orgId) {
44+
ensureObjectId(orgId, '--org-id');
45+
if (orgId !== instance.org_id) {
46+
throw new Error(`Instance ${instanceId} belongs to organization ${instance.org_id}, not ${orgId}.`);
47+
}
6548
}
6649

67-
if (projectId && projectId !== instance.app_id) {
68-
throw new Error(`Instance ${instanceId} belongs to project ${instance.app_id}, not ${projectId}.`);
50+
if (projectId) {
51+
ensureObjectId(projectId, '--project-id');
52+
if (projectId !== instance.app_id) {
53+
throw new Error(`Instance ${instanceId} belongs to project ${instance.app_id}, not ${projectId}.`);
54+
}
6955
}
7056

7157
return {

0 commit comments

Comments
 (0)