@powersync/service-core 0.0.0-dev-20260223082111 → 0.0.0-dev-20260225093637
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 +22 -5
- package/dist/api/diagnostics.js +11 -4
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +13 -2
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/config-command.js +2 -2
- package/dist/entry/commands/config-command.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +84 -0
- package/dist/routes/endpoints/admin.d.ts +168 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +2 -1
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +5 -0
- package/dist/sync/BucketChecksumState.js +85 -10
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +13 -0
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +4 -4
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +2 -2
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +3 -3
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/config/types.js.map +1 -1
- package/dist/util/env.d.ts +1 -0
- package/dist/util/env.js +5 -0
- package/dist/util/env.js.map +1 -1
- package/package.json +5 -5
- package/src/api/diagnostics.ts +12 -4
- package/src/entry/commands/compact-action.ts +15 -2
- package/src/entry/commands/config-command.ts +3 -3
- package/src/storage/SyncRulesBucketStorage.ts +2 -1
- package/src/sync/BucketChecksumState.ts +127 -15
- package/src/util/config/collectors/config-collector.ts +16 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +5 -5
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +4 -4
- package/src/util/config/types.ts +1 -2
- package/src/util/env.ts +5 -0
- package/test/src/config.test.ts +115 -0
- package/test/src/routes/admin.test.ts +48 -0
- package/test/src/routes/mocks.ts +22 -1
- package/test/src/sync/BucketChecksumState.test.ts +200 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import { RunnerConfig, SyncRulesConfig } from '../../types.js';
|
|
2
3
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
3
|
-
import { configFile } from '@powersync/service-types';
|
|
4
4
|
|
|
5
5
|
export class Base64SyncRulesCollector extends SyncRulesCollector {
|
|
6
6
|
get name(): string {
|
|
@@ -8,15 +8,15 @@ export class Base64SyncRulesCollector extends SyncRulesCollector {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise<SyncRulesConfig | null> {
|
|
11
|
-
const {
|
|
12
|
-
if (!
|
|
11
|
+
const { sync_config_base64 } = runnerConfig;
|
|
12
|
+
if (!sync_config_base64) {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
present: true,
|
|
18
|
-
exit_on_error: baseConfig.
|
|
19
|
-
content: Buffer.from(
|
|
18
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
19
|
+
content: Buffer.from(sync_config_base64, 'base64').toString()
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
import { RunnerConfig, SyncRulesConfig } from '../../types.js';
|
|
3
4
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
4
|
-
import { configFile } from '@powersync/service-types';
|
|
5
5
|
|
|
6
6
|
export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
7
7
|
get name(): string {
|
|
@@ -9,7 +9,7 @@ export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise<SyncRulesConfig | null> {
|
|
12
|
-
const sync_path = baseConfig.
|
|
12
|
+
const sync_path = baseConfig.sync_config?.path;
|
|
13
13
|
if (!sync_path) {
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
@@ -20,7 +20,7 @@ export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
|
20
20
|
// Only persist the path here, and load on demand using `loadSyncRules()`.
|
|
21
21
|
return {
|
|
22
22
|
present: true,
|
|
23
|
-
exit_on_error: baseConfig.
|
|
23
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
24
24
|
path: config_path ? path.resolve(path.dirname(config_path), sync_path) : sync_path
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import { SyncRulesConfig } from '../../types.js';
|
|
2
3
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
3
|
-
import { configFile } from '@powersync/service-types';
|
|
4
4
|
|
|
5
5
|
export class InlineSyncRulesCollector extends SyncRulesCollector {
|
|
6
6
|
get name(): string {
|
|
@@ -8,15 +8,15 @@ export class InlineSyncRulesCollector extends SyncRulesCollector {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async collect(baseConfig: configFile.PowerSyncConfig): Promise<SyncRulesConfig | null> {
|
|
11
|
-
const content = baseConfig
|
|
11
|
+
const content = baseConfig?.sync_config?.content;
|
|
12
12
|
if (!content) {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
present: true,
|
|
18
|
-
exit_on_error: true,
|
|
19
|
-
...baseConfig.
|
|
18
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
19
|
+
...baseConfig.sync_config
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/util/config/types.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { configFile } from '@powersync/service-types';
|
|
2
2
|
import { CompoundKeyCollector } from '../../auth/CompoundKeyCollector.js';
|
|
3
|
-
import { KeySpec } from '../../auth/KeySpec.js';
|
|
4
3
|
import { KeyStore } from '../../auth/KeyStore.js';
|
|
5
4
|
|
|
6
5
|
export enum ServiceRunner {
|
|
@@ -12,7 +11,7 @@ export enum ServiceRunner {
|
|
|
12
11
|
export type RunnerConfig = {
|
|
13
12
|
config_path?: string;
|
|
14
13
|
config_base64?: string;
|
|
15
|
-
|
|
14
|
+
sync_config_base64?: string;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
17
|
export type MigrationContext = {
|
package/src/util/env.ts
CHANGED
|
@@ -12,9 +12,14 @@ export const env = utils.collectEnvironmentVariables({
|
|
|
12
12
|
*/
|
|
13
13
|
POWERSYNC_CONFIG_B64: utils.type.string.optional(),
|
|
14
14
|
/**
|
|
15
|
+
* @deprecated use POWERSYNC_SYNC_CONFIG_B64 instead.
|
|
15
16
|
* Base64 encoded contents of sync rules YAML
|
|
16
17
|
*/
|
|
17
18
|
POWERSYNC_SYNC_RULES_B64: utils.type.string.optional(),
|
|
19
|
+
/**
|
|
20
|
+
* Base64 encoded contents of sync rules YAML
|
|
21
|
+
*/
|
|
22
|
+
POWERSYNC_SYNC_CONFIG_B64: utils.type.string.optional(),
|
|
18
23
|
/**
|
|
19
24
|
* Runner to be started in this process
|
|
20
25
|
*/
|
package/test/src/config.test.ts
CHANGED
|
@@ -69,6 +69,7 @@ describe('Config', () => {
|
|
|
69
69
|
|
|
70
70
|
expect(config.api_parameters.max_buckets_per_connection).toBe(1);
|
|
71
71
|
});
|
|
72
|
+
|
|
72
73
|
it('should throw YAML validation error for invalid base64 config', {}, async () => {
|
|
73
74
|
const yamlConfig = /* yaml */ `
|
|
74
75
|
# PowerSync config
|
|
@@ -86,4 +87,118 @@ describe('Config', () => {
|
|
|
86
87
|
})
|
|
87
88
|
).rejects.toThrow(/YAML Error:[\s\S]*Attempting to substitute environment variable INVALID_VAR/);
|
|
88
89
|
});
|
|
90
|
+
|
|
91
|
+
it('should resolve inline sync config', async () => {
|
|
92
|
+
const yamlConfig = /* yaml */ `
|
|
93
|
+
# PowerSync config
|
|
94
|
+
replication:
|
|
95
|
+
connections: []
|
|
96
|
+
storage:
|
|
97
|
+
type: mongodb
|
|
98
|
+
sync_config:
|
|
99
|
+
content: |
|
|
100
|
+
config:
|
|
101
|
+
edition: 2
|
|
102
|
+
streams:
|
|
103
|
+
a:
|
|
104
|
+
query: SELECT * FROM users
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const collector = new CompoundConfigCollector();
|
|
108
|
+
|
|
109
|
+
const result = await collector.collectConfig({
|
|
110
|
+
config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result.sync_rules).toEqual({
|
|
114
|
+
present: true,
|
|
115
|
+
exit_on_error: true,
|
|
116
|
+
content: expect.stringContaining('edition: 2')
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should still resolve inline sync rules', async () => {
|
|
121
|
+
const yamlConfig = /* yaml */ `
|
|
122
|
+
# PowerSync config
|
|
123
|
+
replication:
|
|
124
|
+
connections: []
|
|
125
|
+
storage:
|
|
126
|
+
type: mongodb
|
|
127
|
+
sync_rules:
|
|
128
|
+
content: |
|
|
129
|
+
config:
|
|
130
|
+
edition: 2
|
|
131
|
+
streams:
|
|
132
|
+
a:
|
|
133
|
+
query: SELECT * FROM users
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const collector = new CompoundConfigCollector();
|
|
137
|
+
|
|
138
|
+
const result = await collector.collectConfig({
|
|
139
|
+
config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.sync_rules).toEqual({
|
|
143
|
+
present: true,
|
|
144
|
+
exit_on_error: true,
|
|
145
|
+
content: expect.stringContaining('edition: 2')
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should resolve base64 sync config', async () => {
|
|
150
|
+
const yamlConfig = /* yaml */ `
|
|
151
|
+
# PowerSync config
|
|
152
|
+
replication:
|
|
153
|
+
connections: []
|
|
154
|
+
storage:
|
|
155
|
+
type: mongodb
|
|
156
|
+
`;
|
|
157
|
+
const yamlSyncConfig = /* yaml */ `
|
|
158
|
+
config:
|
|
159
|
+
edition: 2
|
|
160
|
+
streams:
|
|
161
|
+
a:
|
|
162
|
+
query: SELECT * FROM users
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const collector = new CompoundConfigCollector();
|
|
166
|
+
|
|
167
|
+
const result = await collector.collectConfig({
|
|
168
|
+
config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64'),
|
|
169
|
+
sync_config_base64: Buffer.from(yamlSyncConfig, 'utf-8').toString('base64')
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.sync_rules).toEqual({
|
|
173
|
+
present: true,
|
|
174
|
+
exit_on_error: true,
|
|
175
|
+
content: expect.stringContaining('edition: 2')
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should not allow both sync_config and sync_rules', async () => {
|
|
180
|
+
const yamlConfig = /* yaml */ `
|
|
181
|
+
# PowerSync config
|
|
182
|
+
replication:
|
|
183
|
+
connections: []
|
|
184
|
+
storage:
|
|
185
|
+
type: mongodb
|
|
186
|
+
sync_config:
|
|
187
|
+
content: |
|
|
188
|
+
config:
|
|
189
|
+
edition: 2
|
|
190
|
+
sync_rules:
|
|
191
|
+
content: |
|
|
192
|
+
config:
|
|
193
|
+
edition: 2
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
const collector = new CompoundConfigCollector();
|
|
197
|
+
|
|
198
|
+
await expect(
|
|
199
|
+
collector.collectConfig({
|
|
200
|
+
config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
|
|
201
|
+
})
|
|
202
|
+
).rejects.toThrow(/Both `sync_config` and `sync_rules` are present/);
|
|
203
|
+
});
|
|
89
204
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BasicRouterRequest, Context, JwtPayload } from '@/index.js';
|
|
2
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { validate } from '../../../src/routes/endpoints/admin.js';
|
|
5
|
+
import { mockServiceContext } from './mocks.js';
|
|
6
|
+
|
|
7
|
+
describe('admin routes', () => {
|
|
8
|
+
describe('validate', () => {
|
|
9
|
+
it('reports errors with source location', async () => {
|
|
10
|
+
const context: Context = {
|
|
11
|
+
logger: logger,
|
|
12
|
+
service_context: mockServiceContext(null),
|
|
13
|
+
token_payload: new JwtPayload({
|
|
14
|
+
sub: '',
|
|
15
|
+
exp: 0,
|
|
16
|
+
iat: 0
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const request: BasicRouterRequest = {
|
|
21
|
+
headers: {},
|
|
22
|
+
hostname: '',
|
|
23
|
+
protocol: 'http'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const response = await validate.handler({
|
|
27
|
+
context,
|
|
28
|
+
params: {
|
|
29
|
+
sync_rules: `
|
|
30
|
+
bucket_definitions:
|
|
31
|
+
missing_table:
|
|
32
|
+
data:
|
|
33
|
+
- SELECT * FROM missing_table
|
|
34
|
+
`
|
|
35
|
+
},
|
|
36
|
+
request
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(response.errors).toEqual([
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
level: 'warning',
|
|
42
|
+
location: { start_offset: 70, end_offset: 83 },
|
|
43
|
+
message: 'Table public.missing_table not found'
|
|
44
|
+
})
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
package/test/src/routes/mocks.ts
CHANGED
|
@@ -41,8 +41,29 @@ export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | nu
|
|
|
41
41
|
return {
|
|
42
42
|
getParseSyncRulesOptions() {
|
|
43
43
|
return { defaultSchema: 'public' };
|
|
44
|
+
},
|
|
45
|
+
async getSourceConfig() {
|
|
46
|
+
return {
|
|
47
|
+
tag: 'test_tag',
|
|
48
|
+
id: 'test_id',
|
|
49
|
+
type: 'test_type'
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async getConnectionSchema() {
|
|
53
|
+
return [];
|
|
54
|
+
},
|
|
55
|
+
async getConnectionStatus() {
|
|
56
|
+
return {
|
|
57
|
+
id: 'test_id',
|
|
58
|
+
uri: 'http://example.org/',
|
|
59
|
+
connected: true,
|
|
60
|
+
errors: []
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
async getDebugTablesInfo() {
|
|
64
|
+
return [];
|
|
44
65
|
}
|
|
45
|
-
}
|
|
66
|
+
} satisfies Partial<RouteAPI> as unknown as RouteAPI;
|
|
46
67
|
},
|
|
47
68
|
addStopHandler() {
|
|
48
69
|
return () => {};
|
|
@@ -827,6 +827,206 @@ config:
|
|
|
827
827
|
});
|
|
828
828
|
});
|
|
829
829
|
});
|
|
830
|
+
|
|
831
|
+
describe('parameter query result logging', () => {
|
|
832
|
+
test('logs parameter query results for dynamic buckets', async () => {
|
|
833
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
834
|
+
storage.updateTestChecksum({ bucket: 'by_project[1]', checksum: 1, count: 1 });
|
|
835
|
+
storage.updateTestChecksum({ bucket: 'by_project[2]', checksum: 1, count: 1 });
|
|
836
|
+
storage.updateTestChecksum({ bucket: 'by_project[3]', checksum: 1, count: 1 });
|
|
837
|
+
|
|
838
|
+
const logMessages: string[] = [];
|
|
839
|
+
const logData: any[] = [];
|
|
840
|
+
const mockLogger = {
|
|
841
|
+
info: (message: string, data: any) => {
|
|
842
|
+
logMessages.push(message);
|
|
843
|
+
logData.push(data);
|
|
844
|
+
},
|
|
845
|
+
error: () => {},
|
|
846
|
+
warn: () => {},
|
|
847
|
+
debug: () => {}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const state = new BucketChecksumState({
|
|
851
|
+
syncContext,
|
|
852
|
+
tokenPayload: new JwtPayload({ sub: 'u1' }),
|
|
853
|
+
syncRequest,
|
|
854
|
+
syncRules: SYNC_RULES_DYNAMIC,
|
|
855
|
+
bucketStorage: storage,
|
|
856
|
+
logger: mockLogger as any
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
const line = (await state.buildNextCheckpointLine({
|
|
860
|
+
base: storage.makeCheckpoint(1n, (lookups) => {
|
|
861
|
+
return [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
862
|
+
}),
|
|
863
|
+
writeCheckpoint: null,
|
|
864
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
865
|
+
}))!;
|
|
866
|
+
line.advance();
|
|
867
|
+
|
|
868
|
+
expect(logMessages[0]).toContain('param_results: 3');
|
|
869
|
+
expect(logData[0].parameter_query_results).toBe(3);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test('throws error with breakdown when parameter query limit is exceeded', async () => {
|
|
873
|
+
const SYNC_RULES_MULTI = SqlSyncRules.fromYaml(
|
|
874
|
+
`
|
|
875
|
+
bucket_definitions:
|
|
876
|
+
projects:
|
|
877
|
+
parameters: select id from projects where user_id = request.user_id()
|
|
878
|
+
data: []
|
|
879
|
+
tasks:
|
|
880
|
+
parameters: select id from tasks where user_id = request.user_id()
|
|
881
|
+
data: []
|
|
882
|
+
comments:
|
|
883
|
+
parameters: select id from comments where user_id = request.user_id()
|
|
884
|
+
data: []
|
|
885
|
+
`,
|
|
886
|
+
{ defaultSchema: 'public' }
|
|
887
|
+
).config.hydrate({ hydrationState: versionedHydrationState(4) });
|
|
888
|
+
|
|
889
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
890
|
+
|
|
891
|
+
const errorMessages: string[] = [];
|
|
892
|
+
const errorData: any[] = [];
|
|
893
|
+
const mockLogger = {
|
|
894
|
+
info: () => {},
|
|
895
|
+
error: (message: string, data: any) => {
|
|
896
|
+
errorMessages.push(message);
|
|
897
|
+
errorData.push(data);
|
|
898
|
+
},
|
|
899
|
+
warn: () => {},
|
|
900
|
+
debug: () => {}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const smallContext = new SyncContext({
|
|
904
|
+
maxBuckets: 100,
|
|
905
|
+
maxParameterQueryResults: 50,
|
|
906
|
+
maxDataFetchConcurrency: 10
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const state = new BucketChecksumState({
|
|
910
|
+
syncContext: smallContext,
|
|
911
|
+
tokenPayload: new JwtPayload({ sub: 'u1' }),
|
|
912
|
+
syncRequest,
|
|
913
|
+
syncRules: SYNC_RULES_MULTI,
|
|
914
|
+
bucketStorage: storage,
|
|
915
|
+
logger: mockLogger as any
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// Create 60 total results: 30 projects + 20 tasks + 10 comments
|
|
919
|
+
const projectIds = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
|
|
920
|
+
const taskIds = Array.from({ length: 20 }, (_, i) => ({ id: i + 1 }));
|
|
921
|
+
const commentIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
|
|
922
|
+
|
|
923
|
+
for (let i = 1; i <= 30; i++) {
|
|
924
|
+
storage.updateTestChecksum({ bucket: `projects[${i}]`, checksum: 1, count: 1 });
|
|
925
|
+
}
|
|
926
|
+
for (let i = 1; i <= 20; i++) {
|
|
927
|
+
storage.updateTestChecksum({ bucket: `tasks[${i}]`, checksum: 1, count: 1 });
|
|
928
|
+
}
|
|
929
|
+
for (let i = 1; i <= 10; i++) {
|
|
930
|
+
storage.updateTestChecksum({ bucket: `comments[${i}]`, checksum: 1, count: 1 });
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
await expect(
|
|
934
|
+
state.buildNextCheckpointLine({
|
|
935
|
+
base: storage.makeCheckpoint(1n, (lookups) => {
|
|
936
|
+
const lookup = lookups[0];
|
|
937
|
+
const lookupName = lookup.values[0];
|
|
938
|
+
if (lookupName === 'projects') {
|
|
939
|
+
return projectIds;
|
|
940
|
+
} else if (lookupName === 'tasks') {
|
|
941
|
+
return taskIds;
|
|
942
|
+
} else {
|
|
943
|
+
return commentIds;
|
|
944
|
+
}
|
|
945
|
+
}),
|
|
946
|
+
writeCheckpoint: null,
|
|
947
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
948
|
+
})
|
|
949
|
+
).rejects.toThrow('Too many parameter query results: 60 (limit of 50)');
|
|
950
|
+
|
|
951
|
+
// Verify error log includes breakdown
|
|
952
|
+
expect(errorMessages[0]).toContain('Parameter query results by definition:');
|
|
953
|
+
expect(errorMessages[0]).toContain('projects: 30');
|
|
954
|
+
expect(errorMessages[0]).toContain('tasks: 20');
|
|
955
|
+
expect(errorMessages[0]).toContain('comments: 10');
|
|
956
|
+
|
|
957
|
+
expect(errorData[0].parameter_query_results).toBe(60);
|
|
958
|
+
expect(errorData[0].parameter_query_results_by_definition).toEqual({
|
|
959
|
+
projects: 30,
|
|
960
|
+
tasks: 20,
|
|
961
|
+
comments: 10
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
test('limits breakdown to top 10 definitions', async () => {
|
|
966
|
+
// Create sync rules with 15 different definitions with dynamic parameters
|
|
967
|
+
let yamlDefinitions = 'bucket_definitions:\n';
|
|
968
|
+
for (let i = 1; i <= 15; i++) {
|
|
969
|
+
yamlDefinitions += ` def${i}:\n`;
|
|
970
|
+
yamlDefinitions += ` parameters: select id from def${i}_table where user_id = request.user_id()\n`;
|
|
971
|
+
yamlDefinitions += ` data: []\n`;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
|
|
975
|
+
hydrationState: versionedHydrationState(5)
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
979
|
+
|
|
980
|
+
const errorMessages: string[] = [];
|
|
981
|
+
const mockLogger = {
|
|
982
|
+
info: () => {},
|
|
983
|
+
error: (message: string) => {
|
|
984
|
+
errorMessages.push(message);
|
|
985
|
+
},
|
|
986
|
+
warn: () => {},
|
|
987
|
+
debug: () => {}
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const smallContext = new SyncContext({
|
|
991
|
+
maxBuckets: 100,
|
|
992
|
+
maxParameterQueryResults: 10,
|
|
993
|
+
maxDataFetchConcurrency: 10
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
const state = new BucketChecksumState({
|
|
997
|
+
syncContext: smallContext,
|
|
998
|
+
tokenPayload: new JwtPayload({ sub: 'u1' }),
|
|
999
|
+
syncRequest,
|
|
1000
|
+
syncRules: SYNC_RULES_MANY,
|
|
1001
|
+
bucketStorage: storage,
|
|
1002
|
+
logger: mockLogger as any
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Each definition creates one bucket, total 15 buckets
|
|
1006
|
+
for (let i = 1; i <= 15; i++) {
|
|
1007
|
+
storage.updateTestChecksum({ bucket: `def${i}[${i}]`, checksum: 1, count: 1 });
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
await expect(
|
|
1011
|
+
state.buildNextCheckpointLine({
|
|
1012
|
+
base: storage.makeCheckpoint(1n, (lookups) => {
|
|
1013
|
+
// Return one result for each definition
|
|
1014
|
+
return [{ id: 1 }];
|
|
1015
|
+
}),
|
|
1016
|
+
writeCheckpoint: null,
|
|
1017
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
1018
|
+
})
|
|
1019
|
+
).rejects.toThrow('Too many parameter query results: 15 (limit of 10)');
|
|
1020
|
+
|
|
1021
|
+
// Verify only top 10 are shown
|
|
1022
|
+
const errorMessage = errorMessages[0];
|
|
1023
|
+
expect(errorMessage).toContain('... and 5 more results from 5 definitions');
|
|
1024
|
+
|
|
1025
|
+
// Count how many definitions are listed (should be exactly 10)
|
|
1026
|
+
const defMatches = errorMessage.match(/def\d+:/g);
|
|
1027
|
+
expect(defMatches?.length).toBe(10);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
830
1030
|
});
|
|
831
1031
|
|
|
832
1032
|
class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
|