@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.
- package/CHANGELOG.md +29 -13
- package/dist/auth/JwtPayload.d.ts +2 -2
- package/dist/auth/JwtPayload.js +2 -2
- package/dist/auth/JwtPayload.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +4 -0
- package/dist/replication/AbstractReplicator.js +8 -2
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +0 -4
- package/dist/storage/StorageVersionConfig.js +0 -4
- package/dist/storage/StorageVersionConfig.js.map +1 -1
- package/dist/storage/SyncConfigParser.js +14 -11
- package/dist/storage/SyncConfigParser.js.map +1 -1
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +3 -3
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -1
- package/package.json +5 -5
- package/src/auth/JwtPayload.ts +2 -2
- package/src/replication/AbstractReplicator.ts +9 -2
- package/src/storage/StorageVersionConfig.ts +0 -5
- package/src/storage/SyncConfigParser.ts +17 -14
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +6 -3
- package/test/src/AbstractReplicator.test.ts +70 -0
- package/test/src/SyncConfigParser.test.ts +37 -58
- package/test/src/diagnostics.test.ts +4 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
DEFAULT_HYDRATION_STATE,
|
|
9
9
|
HydratedSyncConfig,
|
|
10
10
|
nodeSqlite,
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
41
|
-
if (
|
|
42
|
-
|
|
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
|
|
52
|
-
|
|
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
|
|
76
|
-
(config as { config?: {
|
|
53
|
+
const sourceTables = normalizeSourceTableConfig(
|
|
54
|
+
(config as { config?: { source_table_options?: unknown } }).config?.source_table_options
|
|
77
55
|
);
|
|
78
|
-
for (const [
|
|
79
|
-
if (
|
|
80
|
-
context.parsedConfig.
|
|
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.
|
|
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.
|
|
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) }).
|
|
203
|
-
).toEqual(restored.config.
|
|
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.
|
|
187
|
+
(malformed.plan.sourceTableConfig!.orders as any).sample = 'wrong';
|
|
207
188
|
expect(() => restore(parser, malformed)).toThrow();
|
|
208
|
-
(malformed.plan.
|
|
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.
|
|
193
|
+
expect(restore(parser, null).config.sourceTableConfig).toEqual(restored.config.sourceTableConfig);
|
|
213
194
|
});
|
|
214
195
|
|
|
215
|
-
test('
|
|
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).
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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]).
|
|
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
|
-
|
|
211
|
+
config:
|
|
212
|
+
edition: 3
|
|
213
|
+
streams:
|
|
212
214
|
global:
|
|
213
|
-
|
|
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();
|