@powersync/service-core 1.16.3 → 1.18.0
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 +33 -0
- package/dist/api/diagnostics.js +17 -8
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/modules/loader.d.ts +14 -0
- package/dist/modules/loader.js +34 -0
- package/dist/modules/loader.js.map +1 -0
- package/dist/modules/modules-index.d.ts +1 -0
- package/dist/modules/modules-index.js +1 -0
- package/dist/modules/modules-index.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +26 -0
- package/dist/routes/endpoints/admin.d.ts +42 -0
- package/dist/routes/endpoints/admin.js +2 -2
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +7 -0
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +10 -0
- package/dist/routes/endpoints/sync-stream.js +10 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +1 -0
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +1 -0
- package/dist/storage/ReportStorage.d.ts +5 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js +2 -2
- package/dist/sync/sync.js.map +1 -1
- package/dist/util/param-logging.d.ts +23 -0
- package/dist/util/param-logging.js +40 -0
- package/dist/util/param-logging.js.map +1 -0
- package/dist/util/protocol-types.d.ts +6 -2
- package/dist/util/protocol-types.js +4 -0
- package/dist/util/protocol-types.js.map +1 -1
- package/package.json +6 -6
- package/src/api/diagnostics.ts +20 -11
- package/src/modules/loader.ts +47 -0
- package/src/modules/modules-index.ts +1 -0
- package/src/routes/endpoints/admin.ts +2 -2
- package/src/routes/endpoints/socket-route.ts +9 -0
- package/src/routes/endpoints/sync-stream.ts +13 -1
- package/src/storage/BucketStorageBatch.ts +2 -0
- package/src/storage/PersistedSyncRulesContent.ts +1 -0
- package/src/storage/ReportStorage.ts +7 -0
- package/src/storage/SyncRulesBucketStorage.ts +1 -1
- package/src/sync/sync.ts +4 -10
- package/src/util/param-logging.ts +60 -0
- package/src/util/protocol-types.ts +7 -2
- package/test/src/module-loader.test.ts +102 -0
- package/test/src/routes/stream.test.ts +94 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { AbstractModule, loadModules, ServiceContextContainer, TearDownOptions } from '@/index.js';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
interface MockConfig {
|
|
5
|
+
connections?: { type: string }[];
|
|
6
|
+
storage: { type: string };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class MockMySQLModule extends AbstractModule {
|
|
10
|
+
constructor() {
|
|
11
|
+
super({ name: 'MySQLModule' });
|
|
12
|
+
}
|
|
13
|
+
async initialize(context: ServiceContextContainer): Promise<void> {}
|
|
14
|
+
async teardown(options: TearDownOptions): Promise<void> {}
|
|
15
|
+
}
|
|
16
|
+
class MockPostgresModule extends AbstractModule {
|
|
17
|
+
constructor() {
|
|
18
|
+
super({ name: 'PostgresModule' });
|
|
19
|
+
}
|
|
20
|
+
async initialize(context: ServiceContextContainer): Promise<void> {}
|
|
21
|
+
async teardown(options: TearDownOptions): Promise<void> {}
|
|
22
|
+
}
|
|
23
|
+
class MockPostgresStorageModule extends AbstractModule {
|
|
24
|
+
constructor() {
|
|
25
|
+
super({ name: 'PostgresStorageModule' });
|
|
26
|
+
}
|
|
27
|
+
async initialize(context: ServiceContextContainer): Promise<void> {}
|
|
28
|
+
async teardown(options: TearDownOptions): Promise<void> {}
|
|
29
|
+
}
|
|
30
|
+
const mockLoaders = {
|
|
31
|
+
connection: {
|
|
32
|
+
mysql: async () => {
|
|
33
|
+
return new MockMySQLModule();
|
|
34
|
+
},
|
|
35
|
+
postgresql: async () => {
|
|
36
|
+
return new MockPostgresModule();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
storage: {
|
|
40
|
+
postgresql: async () => {
|
|
41
|
+
return new MockPostgresStorageModule();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe('module loader', () => {
|
|
47
|
+
it('should load all modules defined in connections and storage', async () => {
|
|
48
|
+
const config: MockConfig = {
|
|
49
|
+
connections: [{ type: 'mysql' }, { type: 'postgresql' }],
|
|
50
|
+
storage: { type: 'postgresql' }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const modules = await loadModules(config as any, mockLoaders);
|
|
54
|
+
|
|
55
|
+
expect(modules.length).toBe(3);
|
|
56
|
+
expect(modules[0]).toBeInstanceOf(MockMySQLModule);
|
|
57
|
+
expect(modules[1]).toBeInstanceOf(MockPostgresModule);
|
|
58
|
+
expect(modules[2]).toBeInstanceOf(MockPostgresStorageModule);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle duplicate connection types (e.g., mysql used twice)', async () => {
|
|
62
|
+
const config: MockConfig = {
|
|
63
|
+
connections: [{ type: 'mysql' }, { type: 'postgresql' }, { type: 'mysql' }], // mysql duplicated
|
|
64
|
+
storage: { type: 'postgresql' }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const modules = await loadModules(config as any, mockLoaders);
|
|
68
|
+
|
|
69
|
+
// Expect 3 modules: mysql, postgresql, postgresql-storage
|
|
70
|
+
expect(modules.length).toBe(3);
|
|
71
|
+
expect(modules.filter((m) => m instanceof MockMySQLModule).length).toBe(1);
|
|
72
|
+
expect(modules.filter((m) => m instanceof MockPostgresModule).length).toBe(1);
|
|
73
|
+
expect(modules.filter((m) => m instanceof MockPostgresStorageModule).length).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should throw an error if any modules are not found in ModuleMap', async () => {
|
|
77
|
+
const config: MockConfig = {
|
|
78
|
+
connections: [{ type: 'mysql' }, { type: 'redis' }],
|
|
79
|
+
storage: { type: 'postgresql' }
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await expect(loadModules(config as any, mockLoaders)).rejects.toThrowError();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw an error if one dynamic connection module import fails', async () => {
|
|
86
|
+
const config: MockConfig = {
|
|
87
|
+
connections: [{ type: 'mysql' }],
|
|
88
|
+
storage: { type: 'postgresql' }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const loaders = {
|
|
92
|
+
connection: {
|
|
93
|
+
mysql: async () => {
|
|
94
|
+
throw new Error('Failed to load MySQL module');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
storage: mockLoaders.storage
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await expect(loadModules(config as any, loaders)).rejects.toThrowError('Failed to load MySQL module');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BasicRouterRequest, Context, SyncRulesBucketStorage } from '@/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
|
|
3
3
|
import { SqlSyncRules } from '@powersync/service-sync-rules';
|
|
4
4
|
import { Readable, Writable } from 'stream';
|
|
5
5
|
import { pipeline } from 'stream/promises';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import winston from 'winston';
|
|
7
8
|
import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
|
|
9
|
+
import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, limitParamsForLogging } from '../../../src/util/param-logging.js';
|
|
8
10
|
import { mockServiceContext } from './mocks.js';
|
|
9
11
|
|
|
10
12
|
describe('Stream Route', () => {
|
|
@@ -77,6 +79,97 @@ describe('Stream Route', () => {
|
|
|
77
79
|
const r = await drainWithTimeout(stream).catch((error) => error);
|
|
78
80
|
expect(r.message).toContain('Simulated storage error');
|
|
79
81
|
});
|
|
82
|
+
|
|
83
|
+
it('logs the application metadata', async () => {
|
|
84
|
+
const storage = {
|
|
85
|
+
getParsedSyncRules() {
|
|
86
|
+
return new SqlSyncRules('bucket_definitions: {}');
|
|
87
|
+
},
|
|
88
|
+
watchCheckpointChanges: async function* (options) {
|
|
89
|
+
throw new Error('Simulated storage error');
|
|
90
|
+
}
|
|
91
|
+
} as Partial<SyncRulesBucketStorage>;
|
|
92
|
+
const serviceContext = mockServiceContext(storage);
|
|
93
|
+
|
|
94
|
+
// Create a custom format to capture log info objects (which include defaultMeta)
|
|
95
|
+
const capturedLogs: any[] = [];
|
|
96
|
+
const captureFormat = winston.format((info) => {
|
|
97
|
+
// Capture the info object which includes defaultMeta merged in
|
|
98
|
+
capturedLogs.push({ ...info });
|
|
99
|
+
return info;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Create a test logger with the capture format
|
|
103
|
+
const testLogger = winston.createLogger({
|
|
104
|
+
format: winston.format.combine(captureFormat(), winston.format.json()),
|
|
105
|
+
transports: [new winston.transports.Console()]
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const context: Context = {
|
|
109
|
+
logger: testLogger,
|
|
110
|
+
service_context: serviceContext,
|
|
111
|
+
token_payload: {
|
|
112
|
+
exp: new Date().getTime() / 1000 + 10000,
|
|
113
|
+
iat: new Date().getTime() / 1000 - 10000,
|
|
114
|
+
sub: 'test-user'
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const request: BasicRouterRequest = {
|
|
119
|
+
headers: {
|
|
120
|
+
'accept-encoding': 'gzip'
|
|
121
|
+
},
|
|
122
|
+
hostname: '',
|
|
123
|
+
protocol: 'http'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const inputMeta = {
|
|
127
|
+
test: 'test',
|
|
128
|
+
long_meta: 'a'.repeat(1000)
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const response = await (syncStreamed.handler({
|
|
132
|
+
context,
|
|
133
|
+
params: {
|
|
134
|
+
app_metadata: inputMeta,
|
|
135
|
+
parameters: {
|
|
136
|
+
user_name: 'bob',
|
|
137
|
+
nested_object: {
|
|
138
|
+
nested_key: 'b'.repeat(1000)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
request
|
|
143
|
+
}) as Promise<RouterResponse>);
|
|
144
|
+
expect(response.status).toEqual(200);
|
|
145
|
+
const stream = response.data as Readable;
|
|
146
|
+
const r = await drainWithTimeout(stream).catch((error) => error);
|
|
147
|
+
expect(r.message).toContain('Simulated storage error');
|
|
148
|
+
|
|
149
|
+
// Find the "Sync stream started" log entry
|
|
150
|
+
const syncStartedLog = capturedLogs.find((log) => log.message === 'Sync stream started');
|
|
151
|
+
expect(syncStartedLog).toBeDefined();
|
|
152
|
+
|
|
153
|
+
// Verify that app_metadata from defaultMeta is present in the log
|
|
154
|
+
expect(syncStartedLog?.app_metadata).toBeDefined();
|
|
155
|
+
expect(syncStartedLog?.app_metadata).toEqual(limitParamsForLogging(inputMeta));
|
|
156
|
+
// Should trim long metadata
|
|
157
|
+
expect(syncStartedLog?.app_metadata.long_meta.length).toEqual(
|
|
158
|
+
DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Verify the explicit log parameters
|
|
162
|
+
expect(syncStartedLog?.client_params).toEqual(
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
user_name: 'bob'
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(typeof syncStartedLog?.client_params.nested_object).toEqual('string');
|
|
169
|
+
expect(syncStartedLog?.client_params.nested_object.length).toEqual(
|
|
170
|
+
DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength
|
|
171
|
+
);
|
|
172
|
+
});
|
|
80
173
|
});
|
|
81
174
|
});
|
|
82
175
|
|