@powersync/service-core 0.0.0-dev-20260929095716 → 0.0.0-dev-20261005120023

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import {
8
8
  DEFAULT_HYDRATION_STATE,
9
9
  HydratedSyncConfig,
10
10
  nodeSqlite,
11
- normalizeConnectionConfig,
11
+ normalizeSourceTableConfig,
12
12
  PrecompiledSyncConfig,
13
13
  SyncConfig
14
14
  } from '@powersync/service-sync-rules';
@@ -16,68 +16,49 @@ import * as sqlite from 'node:sqlite';
16
16
  import { describe, expect, test, vi } from 'vitest';
17
17
 
18
18
  const STREAMS = /* yaml */ `
19
- config: { edition: 3 }
19
+ # Sync config fixture.
20
+ config:
21
+ edition: 3
20
22
  streams:
21
23
  orders:
22
24
  query: SELECT * FROM orders
23
25
  `;
24
26
  const CONFIGURED = /* yaml */ `
27
+ # Sync config fixture.
25
28
  config:
26
29
  edition: 3
27
- connections:
28
- default:
29
- type: example
30
- tables:
31
- orders: { sample: 10 }
30
+ source_table_options:
31
+ orders:
32
+ sample: 10
32
33
  streams:
33
34
  orders:
34
35
  query: SELECT * FROM orders
35
36
  `;
36
37
 
37
- // This fixture represents an external module. Core only knows the connection/table maps; sample is module-owned.
38
+ // This fixture represents an external module. Core only knows the source-table map; sample is module-owned.
38
39
  function additionalParser(): AdditionalSyncConfigParser {
39
40
  const check = (config: SyncConfig) => {
40
- for (const connection of Object.values(config.connectionConfig)) {
41
- if (connection?.type != 'example') continue;
42
- for (const table of Object.values(connection.tables ?? {})) {
43
- if (typeof (table as { sample?: number })?.sample != 'number') throw new Error('Invalid persisted sample.');
44
- if ((table as { sample?: number })?.sample === 13) throw new Error('Sample 13 is unsupported.');
45
- }
41
+ for (const options of Object.values(config.sourceTableConfig)) {
42
+ if (typeof (options as { sample?: number })?.sample != 'number') throw new Error('Invalid persisted sample.');
43
+ if ((options as { sample?: number })?.sample === 13) throw new Error('Sample 13 is unsupported.');
46
44
  }
47
45
  };
48
46
  return {
49
47
  id: 'example.tables',
50
48
  extendJsonSchema({ schema }) {
51
- const map = (schema.properties as any).config.properties.connections;
52
- map.additionalProperties = {
53
- if: { type: 'object', required: ['type'], properties: { type: { const: 'example' } } },
54
- then: {
55
- type: 'object',
56
- additionalProperties: false,
57
- required: ['type'],
58
- properties: {
59
- type: { const: 'example' },
60
- tables: {
61
- type: 'object',
62
- propertyNames: { minLength: 1 },
63
- additionalProperties: {
64
- type: 'object',
65
- additionalProperties: false,
66
- properties: { sample: { type: 'number' } }
67
- }
68
- }
69
- }
70
- },
71
- else: map.additionalProperties
72
- };
49
+ const options = (schema.properties as any).config.properties.source_table_options.additionalProperties;
50
+ options.properties.sample = { type: 'number' };
73
51
  },
74
52
  parse({ config, context }) {
75
- const connections = normalizeConnectionConfig(
76
- (config as { config?: { connections?: unknown } }).config?.connections
53
+ const sourceTables = normalizeSourceTableConfig(
54
+ (config as { config?: { source_table_options?: unknown } }).config?.source_table_options
77
55
  );
78
- for (const [tag, connection] of Object.entries(connections)) {
79
- if (connection?.type != 'example') continue;
80
- context.parsedConfig.connectionConfig = { ...context.parsedConfig.connectionConfig, [tag]: connection };
56
+ for (const [table, options] of Object.entries(sourceTables)) {
57
+ if (!Object.hasOwn(options!, 'sample')) continue;
58
+ context.parsedConfig.sourceTableConfig = {
59
+ ...context.parsedConfig.sourceTableConfig,
60
+ [table]: options
61
+ };
81
62
  const { plan } = context.parsedConfig as PrecompiledSyncConfig;
82
63
  plan.moduleData = { ...plan.moduleData, ['example.tables']: null };
83
64
  }
@@ -127,7 +108,7 @@ describe('service sync config parser', () => {
127
108
  expect(parser.parseContent(CONFIGURED, { defaultSchema: 'app' }).errors).toEqual([]);
128
109
  expect(baseSchema).toEqual(new SqlSyncConfigParser().jsonSchema);
129
110
  const composed = parser.jsonSchema;
130
- delete (composed.properties as any).config.properties.connections;
111
+ delete (composed.properties as any).config.properties.source_table_options;
131
112
  expect(parser.parseContent(CONFIGURED, { defaultSchema: 'app' }).errors).toEqual([]);
132
113
 
133
114
  expect(() => parser.registerParser(additionalParser())).toThrow('already registered');
@@ -197,22 +178,22 @@ describe('service sync config parser', () => {
197
178
  syncConfigParser
198
179
  });
199
180
  const restored = restore();
200
- expect(restored.config.connectionConfig).toEqual(compiled.config.parsed.config.connectionConfig);
181
+ expect(restored.config.sourceTableConfig).toEqual(compiled.config.parsed.config.sourceTableConfig);
201
182
  expect(
202
- restored.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE, sqlite: nodeSqlite(sqlite) }).connectionConfig
203
- ).toEqual(restored.config.connectionConfig);
183
+ restored.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE, sqlite: nodeSqlite(sqlite) }).sourceTableConfig
184
+ ).toEqual(restored.config.sourceTableConfig);
204
185
  expect(() => restore(new SqlSyncConfigParser())).toThrow('Missing required sync config parsers: example.tables');
205
186
  const malformed = structuredClone(compiled.config.plan!);
206
- (malformed.plan.connectionConfig!.default!.tables!.orders as any).sample = 'wrong';
187
+ (malformed.plan.sourceTableConfig!.orders as any).sample = 'wrong';
207
188
  expect(() => restore(parser, malformed)).toThrow();
208
- (malformed.plan.connectionConfig!.default!.tables!.orders as any).sample = 13;
189
+ (malformed.plan.sourceTableConfig!.orders as any).sample = 13;
209
190
  expect(() => restore(parser, malformed)).toThrow('Sample 13');
210
191
  // A compiled plan is authoritative; its failure must not be hidden by reparsing valid source YAML.
211
192
  expect(() => restore(new SqlSyncConfigParser(), null)).toThrow();
212
- expect(restore(parser, null).config.connectionConfig).toEqual(restored.config.connectionConfig);
193
+ expect(restore(parser, null).config.sourceTableConfig).toEqual(restored.config.sourceTableConfig);
213
194
  });
214
195
 
215
- test('checks all dependencies before hooks and ignores unused registered parsers', () => {
196
+ test('reports missing dependencies, runs required installed validators and ignores unused registered parsers', () => {
216
197
  const validatePersisted = vi.fn();
217
198
  const parser = new SqlSyncConfigParser([{ id: 'installed', parse() {}, validatePersisted }]);
218
199
  const { config } = parser.parseContent(STREAMS, { defaultSchema: 'app' });
@@ -222,7 +203,7 @@ describe('service sync config parser', () => {
222
203
  expect(() => parser.validatePersisted({ config, context: { defaultSchema: 'app' } })).toThrow(
223
204
  'Missing required sync config parsers: missing, also-missing'
224
205
  );
225
- expect(validatePersisted).not.toHaveBeenCalled();
206
+ expect(validatePersisted).toHaveBeenCalledTimes(1);
226
207
  });
227
208
 
228
209
  test('restores transformed module state without applying the authoring schema', () => {
@@ -232,9 +213,7 @@ describe('service sync config parser', () => {
232
213
  parse({ context }) {
233
214
  const { plan } = context.parsedConfig as PrecompiledSyncConfig;
234
215
  plan.moduleData = { ...plan.moduleData, ['transformed']: null };
235
- context.parsedConfig.connectionConfig = {
236
- default: { type: 'transformed', tables: { orders: { compiled: true } } }
237
- };
216
+ context.parsedConfig.sourceTableConfig = { orders: { compiled: true } };
238
217
  }
239
218
  }
240
219
  ]);
@@ -248,11 +227,11 @@ describe('service sync config parser', () => {
248
227
  syncConfigParser
249
228
  });
250
229
  expect((restore(parser).config as PrecompiledSyncConfig).plan.moduleData).toEqual({ transformed: null });
251
- expect(restore(parser).config.connectionConfig).toEqual(compiled.config.parsed.config.connectionConfig);
230
+ expect(restore(parser).config.sourceTableConfig).toEqual(compiled.config.parsed.config.sourceTableConfig);
252
231
  expect(() => restore(new SqlSyncConfigParser())).toThrow('Missing required sync config parsers: transformed');
253
232
  });
254
233
 
255
- test('persists module dependencies without connection options and accepts legacy plans without module data', () => {
234
+ test('persists module dependencies without source-table options and accepts legacy plans without module data', () => {
256
235
  const parser = new SqlSyncConfigParser([
257
236
  {
258
237
  id: 'required',
@@ -305,7 +284,7 @@ describe('service sync config parser', () => {
305
284
  );
306
285
  });
307
286
 
308
- test('reuses equal connection options but requires replacement for additions, changes and removals', () => {
287
+ test('reuses equal source-table options but requires replacement for additions, changes and removals', () => {
309
288
  const first = deployOptions();
310
289
  const same = deployOptions(CONFIGURED.replace('SELECT * FROM orders', 'SELECT id FROM orders'));
311
290
  const changed = deployOptions(CONFIGURED.replace('sample: 10', 'sample: 20'));
@@ -316,7 +295,7 @@ describe('service sync config parser', () => {
316
295
  expect(isCompatible([absent.config.plan], first.config, logger)).toBe(false);
317
296
  });
318
297
 
319
- test('merges equal connection config once', () => {
298
+ test('merges equal source-table config once', () => {
320
299
  const first = deployOptions().config.parsed.config;
321
300
  const same = deployOptions(CONFIGURED.replace('SELECT * FROM orders', 'SELECT id FROM orders')).config.parsed
322
301
  .config;
@@ -325,6 +304,6 @@ describe('service sync config parser', () => {
325
304
  definitions,
326
305
  createParams: { hydrationState: DEFAULT_HYDRATION_STATE, sqlite: nodeSqlite(sqlite) }
327
306
  });
328
- expect(hydrate([first, same]).connectionConfig).toEqual(first.connectionConfig);
307
+ expect(hydrate([first, same]).sourceTableConfig).toEqual(first.sourceTableConfig);
329
308
  });
330
309
  });
@@ -208,10 +208,11 @@ describe('getSyncRulesStatus WAL budget warnings', () => {
208
208
 
209
209
  test('reports each sync config error with its source location when parsing fails', async () => {
210
210
  const yaml = `
211
- bucket_definitions:
211
+ config:
212
+ edition: 3
213
+ streams:
212
214
  global:
213
- data:
214
- - SELECT id FROM test_table
215
+ query: SELECT id FROM test_table
215
216
  unknown_key: true
216
217
  `;
217
218
  const content = makeSyncRulesContent();