Skip to content

Commit cc979fa

Browse files
improve config pull output
1 parent e517f95 commit cc979fa

File tree

4 files changed

+175
-43
lines changed

4 files changed

+175
-43
lines changed

cli/src/api/cloud/fetch-cloud-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function fetchCloudConfig(
1818
org_id: linked.org_id,
1919
id: linked.instance_id
2020
});
21-
const configFromCloud = { _type: 'cloud', ...(instanceConfig.config ?? {}) };
21+
const configFromCloud = { _type: 'cloud', name: instanceConfig.name, ...(instanceConfig.config ?? {}) };
2222
const config = CLICloudConfig.decode(configFromCloud as any);
2323
return { config, syncRules: instanceConfig.sync_rules };
2424
}

cli/src/commands/pull/config.ts

Lines changed: 129 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ux } from '@oclif/core';
22
import { CLICloudConfig } from '@powersync/cli-schemas';
3-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4-
import { join } from 'node:path';
3+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4+
import { dirname, join } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
56
import * as t from 'ts-codec';
6-
import { Document } from 'yaml';
7+
import { Document, isMap, type Node, type Pair, parseDocument, YAMLMap } from 'yaml';
78

89
import {
910
CloudInstanceCommand,
@@ -19,65 +20,152 @@ import { writeCloudLink } from '../../api/cloud/write-cloud-link.js';
1920
const SERVICE_FETCHED_FILENAME = 'service-fetched.yaml';
2021
const SYNC_FETCHED_FILENAME = 'sync-fetched.yaml';
2122

23+
const PULL_CONFIG_HEADER = `# PowerSync Cloud config (fetched from cloud)
24+
# yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json
25+
#
26+
`;
27+
28+
const __dirname = dirname(fileURLToPath(import.meta.url));
29+
const CLOUD_SERVICE_TEMPLATE_PATH = join(
30+
__dirname,
31+
'..',
32+
'..',
33+
'..',
34+
'templates',
35+
'cloud',
36+
'powersync',
37+
'service.yaml'
38+
);
39+
2240
type JSONSchemaObject = {
2341
description?: string;
2442
properties?: Record<string, JSONSchemaObject>;
2543
items?: JSONSchemaObject;
2644
};
2745

28-
function getDescriptionFromSchema(schema: JSONSchemaObject | undefined, path: string[]): string | undefined {
29-
if (!schema || path.length === 0) return schema?.description;
30-
const [head, ...rest] = path;
31-
const next = schema.properties?.[head];
32-
return rest.length === 0 ? next?.description : getDescriptionFromSchema(next, rest);
46+
function pairKey(pair: Pair<unknown, unknown>): string | undefined {
47+
const k = pair.key as { value?: string } | undefined;
48+
return k?.value;
3349
}
3450

35-
function setCommentsOnMap(
36-
map: { items: Array<{ key: unknown; value: unknown }> },
37-
schema: JSONSchemaObject | undefined,
38-
path: string[]
39-
): void {
40-
if (!schema?.properties) return;
41-
for (const pair of map.items) {
42-
const keyStr =
43-
typeof pair.key === 'object' && pair.key !== null && 'value' in pair.key
44-
? String((pair.key as { value: unknown }).value)
45-
: String(pair.key);
46-
const keyPath = [...path, keyStr];
47-
const desc = getDescriptionFromSchema(schema, keyPath);
48-
const value = pair.value as { commentBefore?: string | null; items?: unknown[] };
49-
if (value && typeof value === 'object' && desc) {
50-
value.commentBefore = desc;
51-
}
52-
if (value && typeof value === 'object' && Array.isArray((value as { items?: unknown[] }).items)) {
53-
const subSchema = schema.properties?.[keyStr];
54-
const innerMap = value as { items: Array<{ key: unknown; value: unknown }> };
55-
setCommentsOnMap(innerMap, subSchema, keyPath);
56-
}
51+
/** Find the Pair node for a top-level key in a parsed YAML map. */
52+
function findMapPair(contents: unknown, key: string): Pair<unknown, unknown> | null {
53+
if (!contents || typeof (contents as { items?: unknown[] }).items !== 'object') return null;
54+
const items = (contents as { items: Pair<unknown, unknown>[] }).items;
55+
const pair = items.find((p) => pairKey(p) === key);
56+
return pair ?? null;
57+
}
58+
59+
/** Index of the pair with the given key in the map's items, or -1. */
60+
function findMapPairIndex(map: YAMLMap, key: string): number {
61+
const items = map.items ?? [];
62+
return items.findIndex((p) => pairKey(p) === key);
63+
}
64+
65+
/** Insert a pair into the map at the given index (or append if index >= items.length). */
66+
function insertPair(map: YAMLMap, index: number, pair: Pair<unknown, unknown>): void {
67+
const items = map.items ?? [];
68+
if (index < 0 || index >= items.length) {
69+
items.push(pair);
70+
} else {
71+
items.splice(index, 0, pair);
5772
}
5873
}
5974

60-
const PULL_CONFIG_HEADER = `# PowerSync Cloud config (fetched from cloud)
61-
# yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json
62-
#
63-
`;
75+
function hasSection(config: t.Decoded<typeof CLICloudConfig>, key: 'replication' | 'client_auth'): boolean {
76+
const c = config as Record<string, unknown>;
77+
return c[key] !== undefined && c[key] !== null;
78+
}
6479

65-
function getSchema(): JSONSchemaObject {
80+
function formatServiceYamlWithComments(config: t.Decoded<typeof CLICloudConfig>): string {
81+
let schema: JSONSchemaObject;
6682
try {
67-
return (t.generateJSONSchema(CLICloudConfig) as JSONSchemaObject) ?? {};
83+
schema = (t.generateJSONSchema(CLICloudConfig) as JSONSchemaObject) ?? {};
6884
} catch {
69-
return {};
85+
schema = {};
86+
}
87+
88+
function getDescriptionFromSchema(s: JSONSchemaObject | undefined, path: string[]): string | undefined {
89+
if (!s || path.length === 0) return s?.description;
90+
const [head, ...rest] = path;
91+
const next = s.properties?.[head];
92+
return rest.length === 0 ? next?.description : getDescriptionFromSchema(next, rest);
93+
}
94+
95+
function setCommentsOnMap(
96+
map: { items: Array<{ key: unknown; value: unknown }> },
97+
s: JSONSchemaObject | undefined,
98+
path: string[]
99+
): void {
100+
if (!s?.properties) return;
101+
for (const pair of map.items) {
102+
const keyStr =
103+
typeof pair.key === 'object' && pair.key !== null && 'value' in pair.key
104+
? String((pair.key as { value: unknown }).value)
105+
: String(pair.key);
106+
const keyPath = [...path, keyStr];
107+
const desc = getDescriptionFromSchema(s, keyPath);
108+
const value = pair.value as { commentBefore?: string | null; items?: unknown[] };
109+
if (value && typeof value === 'object' && desc) {
110+
value.commentBefore = desc;
111+
}
112+
if (value && typeof value === 'object' && Array.isArray((value as { items?: unknown[] }).items)) {
113+
const subSchema = s.properties?.[keyStr];
114+
const innerMap = value as { items: Array<{ key: unknown; value: unknown }> };
115+
setCommentsOnMap(innerMap, subSchema, keyPath);
116+
}
117+
}
70118
}
71-
}
72119

73-
function formatServiceYamlWithComments(config: t.Decoded<typeof CLICloudConfig>): string {
74-
const schema = getSchema();
75120
const doc = new Document(config);
76121
const contents = doc.contents as { items?: Array<{ key: unknown; value: unknown }> } | null;
77122
if (contents && typeof contents === 'object' && Array.isArray(contents.items) && contents.items.length > 0) {
78123
setCommentsOnMap({ items: contents.items }, Object.keys(schema).length > 0 ? schema : undefined, []);
79124
}
80-
return PULL_CONFIG_HEADER + doc.toString();
125+
126+
let templateDoc: ReturnType<typeof parseDocument> | null = null;
127+
try {
128+
templateDoc = parseDocument(readFileSync(CLOUD_SERVICE_TEMPLATE_PATH, 'utf8'));
129+
} catch {
130+
// template missing or parse failed
131+
}
132+
133+
if (templateDoc?.contents && isMap(doc.contents)) {
134+
const outMap = doc.contents as YAMLMap;
135+
const templateMap = templateDoc.contents as YAMLMap;
136+
const replicationPair = findMapPair(templateMap, 'replication');
137+
const clientAuthPair = findMapPair(templateMap, 'client_auth');
138+
139+
const rep = config.replication as { connections?: unknown[] } | undefined;
140+
const useTemplateReplication = !rep || !Array.isArray(rep.connections) || rep.connections.length === 0;
141+
if (useTemplateReplication && replicationPair) {
142+
const repIdx = findMapPairIndex(outMap, 'replication');
143+
if (repIdx >= 0) {
144+
outMap.items[repIdx] = replicationPair;
145+
} else {
146+
const regionIdx = findMapPairIndex(outMap, 'region');
147+
insertPair(outMap, regionIdx >= 0 ? regionIdx + 1 : 0, replicationPair);
148+
}
149+
}
150+
151+
if (!hasSection(config, 'client_auth') && clientAuthPair) {
152+
insertPair(outMap, outMap.items?.length ?? 0, clientAuthPair);
153+
}
154+
}
155+
156+
let out = PULL_CONFIG_HEADER + doc.toString();
157+
158+
if (hasSection(config, 'client_auth') && templateDoc?.contents) {
159+
const templateMap = templateDoc.contents as YAMLMap;
160+
const clientAuthPair = findMapPair(templateMap, 'client_auth');
161+
if (clientAuthPair?.value != null) {
162+
const commentDoc = new Document();
163+
commentDoc.contents = clientAuthPair.value as Node;
164+
out = out.replace(/\n?$/, '\n') + commentDoc.toString().replace(/\n$/, '');
165+
}
166+
}
167+
168+
return out;
81169
}
82170

83171
export default class PullConfig extends CloudInstanceCommand {

examples/cloud/basic-cloud-pull/powersync/service.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,47 @@ client_auth:
2121
supabase: false
2222
additional_audiences: []
2323
allow_temporary_tokens: true
24+
25+
# PowerSync will use the same JWT secret as Supabase.
26+
# supabase: false
27+
28+
# Legacy: If your Supabase project does not use the new JWT signing keys, you must provide your project's legacy JWT secret to use Supabase Auth. Get it from your project's API settings in the Supabase Dashboard.
29+
# supabase_jwt_secret:
30+
# secret: !env POWERSYNC_SUPABASE_JWT_SECRET
31+
32+
# Additional audiences to accept when validating incoming JWT tokens (the instance domain is always accepted)
33+
# additional_audiences: []
34+
35+
# Enables development tokens to be generated and accepted by the instance
36+
# allow_temporary_tokens: false
37+
38+
# URL to a JSON Web Key Set (JWKS) endpoint; the instance fetches public keys from this URL to verify JWT signatures from clients.
39+
# jwks_uri: https://example.com/jwks.json
40+
# Inline JSON Web Key Set; provide keys directly instead of (or in addition to) jwks_uri to verify JWT signatures.
41+
# jwks:
42+
# keys:
43+
# HMAC (symmetric) – Options: HS256 | HS384 | HS512
44+
# - kty: oct
45+
# alg: HS256
46+
# kid: example-key
47+
# k:
48+
# secret: !env POWERSYNC_CLIENT_AUTH_KEY
49+
# RSA – Options for alg: RS256 | RS384 | RS512
50+
# - kty: RSA
51+
# kid: my-rsa-key
52+
# n: "<base64url-modulus>"
53+
# e: "<base64url-exponent>"
54+
# alg: RS256
55+
# EC – Options for crv: P-256 | P-384 | P-512; alg: ES256 | ES384 | ES512
56+
# - kty: EC
57+
# kid: my-ec-key
58+
# crv: P-256
59+
# x: "<base64url-x>"
60+
# y: "<base64url-y>"
61+
# alg: ES256
62+
# OKP (EdDSA) – Options for crv: Ed25519 | Ed448
63+
# - kty: OKP
64+
# kid: my-okp-key
65+
# crv: Ed25519
66+
# x: "<base64url-public-key>"
67+
# alg: EdDSA

packages/cli-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@journeyapps-labs/common-sdk": "1.0.2",
2525
"@powersync/management-client": "0.0.1",
2626
"@powersync/management-types": "0.0.1",
27-
"@powersync/service-client": "0.0.1",
27+
"@powersync/service-client": "0.0.2",
2828
"keychain": "^1.5.0"
2929
},
3030
"devDependencies": {

0 commit comments

Comments
 (0)