@syncular/server 0.0.6-159 → 0.0.6-167
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/dist/blobs/adapters/database.d.ts +26 -9
- package/dist/blobs/adapters/database.d.ts.map +1 -1
- package/dist/blobs/adapters/database.js +65 -21
- package/dist/blobs/adapters/database.js.map +1 -1
- package/dist/blobs/manager.d.ts +60 -3
- package/dist/blobs/manager.d.ts.map +1 -1
- package/dist/blobs/manager.js +227 -56
- package/dist/blobs/manager.js.map +1 -1
- package/dist/blobs/migrate.d.ts.map +1 -1
- package/dist/blobs/migrate.js +16 -8
- package/dist/blobs/migrate.js.map +1 -1
- package/dist/blobs/types.d.ts +4 -0
- package/dist/blobs/types.d.ts.map +1 -1
- package/dist/dialect/helpers.d.ts +3 -0
- package/dist/dialect/helpers.d.ts.map +1 -1
- package/dist/dialect/helpers.js +17 -0
- package/dist/dialect/helpers.js.map +1 -1
- package/dist/handlers/collection.d.ts +0 -2
- package/dist/handlers/collection.d.ts.map +1 -1
- package/dist/handlers/collection.js +5 -56
- package/dist/handlers/collection.js.map +1 -1
- package/dist/handlers/create-handler.d.ts +0 -4
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +6 -34
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +13 -37
- package/dist/notify.js.map +1 -1
- package/dist/proxy/collection.d.ts +0 -2
- package/dist/proxy/collection.d.ts.map +1 -1
- package/dist/proxy/collection.js +2 -17
- package/dist/proxy/collection.js.map +1 -1
- package/dist/proxy/handler.d.ts +1 -1
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +1 -2
- package/dist/proxy/handler.js.map +1 -1
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/oplog.d.ts.map +1 -1
- package/dist/proxy/oplog.js +1 -7
- package/dist/proxy/oplog.js.map +1 -1
- package/dist/prune.d.ts.map +1 -1
- package/dist/prune.js +1 -13
- package/dist/prune.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +186 -54
- package/dist/pull.js.map +1 -1
- package/dist/push.d.ts +1 -1
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +9 -36
- package/dist/push.js.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +71 -23
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks.d.ts +5 -1
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +14 -1
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +1 -13
- package/dist/stats.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts +1 -1
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +3 -16
- package/dist/subscriptions/resolve.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +2 -4
- package/dist/sync.js.map +1 -1
- package/package.json +2 -2
- package/src/blobs/adapters/database.test.ts +7 -0
- package/src/blobs/adapters/database.ts +119 -39
- package/src/blobs/manager.ts +339 -53
- package/src/blobs/migrate.ts +16 -8
- package/src/blobs/types.ts +4 -0
- package/src/dialect/helpers.ts +19 -0
- package/src/handlers/collection.ts +17 -86
- package/src/handlers/create-handler.ts +9 -44
- package/src/notify.ts +15 -40
- package/src/proxy/collection.ts +5 -27
- package/src/proxy/handler.ts +2 -2
- package/src/proxy/index.ts +0 -2
- package/src/proxy/oplog.ts +1 -9
- package/src/prune.ts +1 -12
- package/src/pull.ts +280 -105
- package/src/push.ts +14 -43
- package/src/snapshot-chunks/db-metadata.ts +107 -27
- package/src/snapshot-chunks.ts +18 -0
- package/src/stats.ts +1 -12
- package/src/subscriptions/resolve.ts +4 -20
- package/src/sync.ts +6 -6
package/src/blobs/migrate.ts
CHANGED
|
@@ -21,7 +21,8 @@ async function ensureBlobUploadsSchemaPostgres<DB extends SyncBlobUploadsDb>(
|
|
|
21
21
|
await db.schema
|
|
22
22
|
.createTable('sync_blob_uploads')
|
|
23
23
|
.ifNotExists()
|
|
24
|
-
.addColumn('
|
|
24
|
+
.addColumn('partition_id', 'text', (col) => col.notNull())
|
|
25
|
+
.addColumn('hash', 'text', (col) => col.notNull())
|
|
25
26
|
.addColumn('size', 'bigint', (col) => col.notNull())
|
|
26
27
|
.addColumn('mime_type', 'text', (col) => col.notNull())
|
|
27
28
|
.addColumn('status', 'text', (col) => col.notNull())
|
|
@@ -31,20 +32,21 @@ async function ensureBlobUploadsSchemaPostgres<DB extends SyncBlobUploadsDb>(
|
|
|
31
32
|
)
|
|
32
33
|
.addColumn('expires_at', 'timestamptz', (col) => col.notNull())
|
|
33
34
|
.addColumn('completed_at', 'timestamptz')
|
|
35
|
+
.addPrimaryKeyConstraint('pk_sync_blob_uploads', ['partition_id', 'hash'])
|
|
34
36
|
.execute();
|
|
35
37
|
|
|
36
38
|
await db.schema
|
|
37
39
|
.createIndex('idx_sync_blob_uploads_status')
|
|
38
40
|
.ifNotExists()
|
|
39
41
|
.on('sync_blob_uploads')
|
|
40
|
-
.columns(['status'])
|
|
42
|
+
.columns(['partition_id', 'status'])
|
|
41
43
|
.execute();
|
|
42
44
|
|
|
43
45
|
await db.schema
|
|
44
46
|
.createIndex('idx_sync_blob_uploads_expires_at')
|
|
45
47
|
.ifNotExists()
|
|
46
48
|
.on('sync_blob_uploads')
|
|
47
|
-
.columns(['expires_at'])
|
|
49
|
+
.columns(['partition_id', 'expires_at'])
|
|
48
50
|
.execute();
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -60,7 +62,8 @@ async function ensureBlobUploadsSchemasSqlite<DB extends SyncBlobUploadsDb>(
|
|
|
60
62
|
await db.schema
|
|
61
63
|
.createTable('sync_blob_uploads')
|
|
62
64
|
.ifNotExists()
|
|
63
|
-
.addColumn('
|
|
65
|
+
.addColumn('partition_id', 'text', (col) => col.notNull())
|
|
66
|
+
.addColumn('hash', 'text', (col) => col.notNull())
|
|
64
67
|
.addColumn('size', 'integer', (col) => col.notNull())
|
|
65
68
|
.addColumn('mime_type', 'text', (col) => col.notNull())
|
|
66
69
|
.addColumn('status', 'text', (col) => col.notNull())
|
|
@@ -70,20 +73,21 @@ async function ensureBlobUploadsSchemasSqlite<DB extends SyncBlobUploadsDb>(
|
|
|
70
73
|
)
|
|
71
74
|
.addColumn('expires_at', 'text', (col) => col.notNull())
|
|
72
75
|
.addColumn('completed_at', 'text')
|
|
76
|
+
.addPrimaryKeyConstraint('pk_sync_blob_uploads', ['partition_id', 'hash'])
|
|
73
77
|
.execute();
|
|
74
78
|
|
|
75
79
|
await db.schema
|
|
76
80
|
.createIndex('idx_sync_blob_uploads_status')
|
|
77
81
|
.ifNotExists()
|
|
78
82
|
.on('sync_blob_uploads')
|
|
79
|
-
.columns(['status'])
|
|
83
|
+
.columns(['partition_id', 'status'])
|
|
80
84
|
.execute();
|
|
81
85
|
|
|
82
86
|
await db.schema
|
|
83
87
|
.createIndex('idx_sync_blob_uploads_expires_at')
|
|
84
88
|
.ifNotExists()
|
|
85
89
|
.on('sync_blob_uploads')
|
|
86
|
-
.columns(['expires_at'])
|
|
90
|
+
.columns(['partition_id', 'expires_at'])
|
|
87
91
|
.execute();
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -103,13 +107,15 @@ export async function ensureBlobStorageSchemaPostgres<DB extends SyncBlobDb>(
|
|
|
103
107
|
await db.schema
|
|
104
108
|
.createTable('sync_blobs')
|
|
105
109
|
.ifNotExists()
|
|
106
|
-
.addColumn('
|
|
110
|
+
.addColumn('partition_id', 'text', (col) => col.notNull())
|
|
111
|
+
.addColumn('hash', 'text', (col) => col.notNull())
|
|
107
112
|
.addColumn('size', 'bigint', (col) => col.notNull())
|
|
108
113
|
.addColumn('mime_type', 'text', (col) => col.notNull())
|
|
109
114
|
.addColumn('body', 'bytea', (col) => col.notNull())
|
|
110
115
|
.addColumn('created_at', 'timestamptz', (col) =>
|
|
111
116
|
col.notNull().defaultTo(sql`now()`)
|
|
112
117
|
)
|
|
118
|
+
.addPrimaryKeyConstraint('pk_sync_blobs', ['partition_id', 'hash'])
|
|
113
119
|
.execute();
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -129,13 +135,15 @@ export async function ensureBlobStorageSchemaSqlite<DB extends SyncBlobDb>(
|
|
|
129
135
|
await db.schema
|
|
130
136
|
.createTable('sync_blobs')
|
|
131
137
|
.ifNotExists()
|
|
132
|
-
.addColumn('
|
|
138
|
+
.addColumn('partition_id', 'text', (col) => col.notNull())
|
|
139
|
+
.addColumn('hash', 'text', (col) => col.notNull())
|
|
133
140
|
.addColumn('size', 'integer', (col) => col.notNull())
|
|
134
141
|
.addColumn('mime_type', 'text', (col) => col.notNull())
|
|
135
142
|
.addColumn('body', 'blob', (col) => col.notNull())
|
|
136
143
|
.addColumn('created_at', 'text', (col) =>
|
|
137
144
|
col.notNull().defaultTo(sql`(datetime('now'))`)
|
|
138
145
|
)
|
|
146
|
+
.addPrimaryKeyConstraint('pk_sync_blobs', ['partition_id', 'hash'])
|
|
139
147
|
.execute();
|
|
140
148
|
}
|
|
141
149
|
|
package/src/blobs/types.ts
CHANGED
|
@@ -13,6 +13,8 @@ import type { Generated } from 'kysely';
|
|
|
13
13
|
* Tracks initiated uploads and their completion status.
|
|
14
14
|
*/
|
|
15
15
|
export interface SyncBlobUploadsTable {
|
|
16
|
+
/** Partition/tenant namespace */
|
|
17
|
+
partition_id: string;
|
|
16
18
|
/** SHA-256 hash with prefix: "sha256:<hex>" */
|
|
17
19
|
hash: string;
|
|
18
20
|
/** Expected size in bytes */
|
|
@@ -44,6 +46,8 @@ export interface SyncBlobUploadsDb {
|
|
|
44
46
|
* Stores blob content directly in the database.
|
|
45
47
|
*/
|
|
46
48
|
export interface SyncBlobsTable {
|
|
49
|
+
/** Partition/tenant namespace */
|
|
50
|
+
partition_id: string;
|
|
47
51
|
/** SHA-256 hash with prefix: "sha256:<hex>" */
|
|
48
52
|
hash: string;
|
|
49
53
|
/** Size in bytes */
|
package/src/dialect/helpers.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { StoredScopes } from '@syncular/core';
|
|
8
|
+
import type { ServerSyncDialect } from './types';
|
|
8
9
|
|
|
9
10
|
export function coerceNumber(value: unknown): number | null {
|
|
10
11
|
if (value === null || value === undefined) return null;
|
|
@@ -24,6 +25,24 @@ export function coerceIsoString(value: unknown): string {
|
|
|
24
25
|
return String(value);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
export function parseJsonValue(value: unknown): unknown {
|
|
29
|
+
if (typeof value !== 'string') return value;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function toDialectJsonValue(
|
|
38
|
+
dialect: Pick<ServerSyncDialect, 'family'>,
|
|
39
|
+
value: unknown
|
|
40
|
+
): unknown {
|
|
41
|
+
if (value === null || value === undefined) return null;
|
|
42
|
+
if (dialect.family === 'sqlite') return JSON.stringify(value);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
export function parseScopes(value: unknown): StoredScopes {
|
|
28
47
|
if (value === null || value === undefined) return {};
|
|
29
48
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertKnownTableDependencies,
|
|
3
|
+
createTableLookup,
|
|
4
|
+
topologicallySortTablesByDependencies,
|
|
5
|
+
} from '@syncular/core';
|
|
1
6
|
import type { SyncCoreDb } from '../schema';
|
|
2
7
|
import type { ServerTableHandler, SyncServerAuth } from './types';
|
|
3
8
|
|
|
@@ -13,101 +18,27 @@ export function createServerHandlerCollection<
|
|
|
13
18
|
DB extends SyncCoreDb = SyncCoreDb,
|
|
14
19
|
Auth extends SyncServerAuth = SyncServerAuth,
|
|
15
20
|
>(handlers: ServerTableHandler<DB, Auth>[]): ServerHandlerCollection<DB, Auth> {
|
|
16
|
-
const byTable =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
byTable
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const dep of handler.dependsOn ?? []) {
|
|
27
|
-
if (!byTable.has(dep)) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Table "${handler.table}" depends on unknown table "${dep}"`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
21
|
+
const byTable = createTableLookup(
|
|
22
|
+
handlers,
|
|
23
|
+
(table) => `Table "${table}" is already registered`
|
|
24
|
+
);
|
|
25
|
+
assertKnownTableDependencies(
|
|
26
|
+
handlers,
|
|
27
|
+
byTable,
|
|
28
|
+
(table, dependency) =>
|
|
29
|
+
`Table "${table}" depends on unknown table "${dependency}"`
|
|
30
|
+
);
|
|
34
31
|
|
|
35
32
|
return { handlers, byTable };
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
export function getServerHandler<
|
|
39
|
-
DB extends SyncCoreDb = SyncCoreDb,
|
|
40
|
-
Auth extends SyncServerAuth = SyncServerAuth,
|
|
41
|
-
>(
|
|
42
|
-
collection: ServerHandlerCollection<DB, Auth>,
|
|
43
|
-
table: string
|
|
44
|
-
): ServerTableHandler<DB, Auth> | undefined {
|
|
45
|
-
return collection.byTable.get(table);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getServerHandlerOrThrow<
|
|
49
|
-
DB extends SyncCoreDb = SyncCoreDb,
|
|
50
|
-
Auth extends SyncServerAuth = SyncServerAuth,
|
|
51
|
-
>(
|
|
52
|
-
collection: ServerHandlerCollection<DB, Auth>,
|
|
53
|
-
table: string
|
|
54
|
-
): ServerTableHandler<DB, Auth> {
|
|
55
|
-
const handler = collection.byTable.get(table);
|
|
56
|
-
if (!handler) throw new Error(`Unknown table: ${table}`);
|
|
57
|
-
return handler;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function topoSortTables<
|
|
61
|
-
DB extends SyncCoreDb = SyncCoreDb,
|
|
62
|
-
Auth extends SyncServerAuth = SyncServerAuth,
|
|
63
|
-
>(
|
|
64
|
-
collection: ServerHandlerCollection<DB, Auth>,
|
|
65
|
-
targetTable?: string
|
|
66
|
-
): ServerTableHandler<DB, Auth>[] {
|
|
67
|
-
const visited = new Set<string>();
|
|
68
|
-
const visiting = new Set<string>();
|
|
69
|
-
const sorted: ServerTableHandler<DB, Auth>[] = [];
|
|
70
|
-
|
|
71
|
-
const visit = (table: string) => {
|
|
72
|
-
if (visited.has(table)) return;
|
|
73
|
-
if (visiting.has(table)) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Circular dependency detected involving table "${table}"`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const handler = collection.byTable.get(table);
|
|
80
|
-
if (!handler) {
|
|
81
|
-
throw new Error(`Unknown table: ${table}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
visiting.add(table);
|
|
85
|
-
for (const dep of handler.dependsOn ?? []) {
|
|
86
|
-
visit(dep);
|
|
87
|
-
}
|
|
88
|
-
visiting.delete(table);
|
|
89
|
-
visited.add(table);
|
|
90
|
-
sorted.push(handler);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (targetTable) {
|
|
94
|
-
visit(targetTable);
|
|
95
|
-
return sorted;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
for (const table of collection.byTable.keys()) {
|
|
99
|
-
visit(table);
|
|
100
|
-
}
|
|
101
|
-
return sorted;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
35
|
export function getServerBootstrapOrder<
|
|
105
36
|
DB extends SyncCoreDb = SyncCoreDb,
|
|
106
37
|
Auth extends SyncServerAuth = SyncServerAuth,
|
|
107
38
|
>(
|
|
108
39
|
collection: ServerHandlerCollection<DB, Auth>
|
|
109
40
|
): ServerTableHandler<DB, Auth>[] {
|
|
110
|
-
return
|
|
41
|
+
return topologicallySortTablesByDependencies(collection.byTable);
|
|
111
42
|
}
|
|
112
43
|
|
|
113
44
|
export function getServerBootstrapOrderFor<
|
|
@@ -117,5 +48,5 @@ export function getServerBootstrapOrderFor<
|
|
|
117
48
|
collection: ServerHandlerCollection<DB, Auth>,
|
|
118
49
|
table: string
|
|
119
50
|
): ServerTableHandler<DB, Auth>[] {
|
|
120
|
-
return
|
|
51
|
+
return topologicallySortTablesByDependencies(collection.byTable, table);
|
|
121
52
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
|
-
ScopePattern,
|
|
7
6
|
ScopeValues,
|
|
8
7
|
ScopeValuesFromPatterns,
|
|
9
8
|
ScopeDefinition as SimpleScopeDefinition,
|
|
@@ -15,9 +14,8 @@ import {
|
|
|
15
14
|
applyCodecsToDbRow,
|
|
16
15
|
type ColumnCodecDialect,
|
|
17
16
|
type ColumnCodecSource,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
toTableColumnCodecs,
|
|
17
|
+
createSingleVariableScopeMetadata,
|
|
18
|
+
createTableColumnCodecsResolver,
|
|
21
19
|
} from '@syncular/core';
|
|
22
20
|
import type {
|
|
23
21
|
DeleteQueryBuilder,
|
|
@@ -77,11 +75,6 @@ function isMissingColumnReferenceError(message: string): boolean {
|
|
|
77
75
|
);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
/**
|
|
81
|
-
* Scope definition for a column - maps scope variable to column name.
|
|
82
|
-
*/
|
|
83
|
-
export type ScopeColumnMap = Record<string, string>;
|
|
84
|
-
|
|
85
78
|
/**
|
|
86
79
|
* Options for creating a declarative server handler.
|
|
87
80
|
*/
|
|
@@ -274,44 +267,16 @@ export function createServerHandler<
|
|
|
274
267
|
authorize,
|
|
275
268
|
extractScopes: customExtractScopes,
|
|
276
269
|
} = options;
|
|
277
|
-
const
|
|
270
|
+
const resolveRowCodecs = createTableColumnCodecsResolver(codecs, {
|
|
271
|
+
dialect: codecDialect,
|
|
272
|
+
});
|
|
278
273
|
const primaryKeyColumn = primaryKey as keyof ServerDB[TableName] & string;
|
|
279
274
|
const qualifiedVersionRef = `${table}.${versionColumn}`;
|
|
280
|
-
const resolveTableCodecs = (row: Record<string, unknown>) =>
|
|
281
|
-
|
|
282
|
-
const columns = Object.keys(row);
|
|
283
|
-
if (columns.length === 0) return {};
|
|
284
|
-
const cacheKey = columns.slice().sort().join('\u0000');
|
|
285
|
-
const cached = codecCache.get(cacheKey);
|
|
286
|
-
if (cached) return cached;
|
|
287
|
-
const resolved = toTableColumnCodecs(table, codecs, columns, {
|
|
288
|
-
dialect: codecDialect,
|
|
289
|
-
});
|
|
290
|
-
codecCache.set(cacheKey, resolved);
|
|
291
|
-
return resolved;
|
|
292
|
-
};
|
|
275
|
+
const resolveTableCodecs = (row: Record<string, unknown>) =>
|
|
276
|
+
resolveRowCodecs(table, row);
|
|
293
277
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const scopePatterns = Object.keys(scopeColumnMap) as ScopePattern[];
|
|
297
|
-
const scopeColumns: ScopeColumnMap = {};
|
|
298
|
-
|
|
299
|
-
for (const [pattern, columnName] of Object.entries(scopeColumnMap)) {
|
|
300
|
-
const vars = extractScopeVars(pattern);
|
|
301
|
-
if (vars.length !== 1) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
`Scope pattern "${pattern}" must contain exactly one placeholder (got ${vars.length}).`
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
const varName = vars[0]!;
|
|
307
|
-
const existing = scopeColumns[varName];
|
|
308
|
-
if (existing && existing !== columnName) {
|
|
309
|
-
throw new Error(
|
|
310
|
-
`Scope variable "${varName}" is mapped to multiple columns: "${existing}" and "${columnName}".`
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
scopeColumns[varName] = columnName;
|
|
314
|
-
}
|
|
278
|
+
const { scopePatterns, scopeColumnsByVariable: scopeColumns } =
|
|
279
|
+
createSingleVariableScopeMetadata(scopeDefs);
|
|
315
280
|
|
|
316
281
|
// Default extractScopes from scope columns
|
|
317
282
|
const defaultExtractScopes = (row: Record<string, unknown>): StoredScopes => {
|
package/src/notify.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { randomId } from '@syncular/core';
|
|
12
12
|
import type { Insertable, Kysely } from 'kysely';
|
|
13
|
+
import { coerceNumber, toDialectJsonValue } from './dialect/helpers';
|
|
13
14
|
import type { ServerSyncDialect } from './dialect/types';
|
|
14
15
|
import type { SyncCoreDb } from './schema';
|
|
15
16
|
|
|
@@ -19,31 +20,6 @@ import type { SyncCoreDb } from './schema';
|
|
|
19
20
|
*/
|
|
20
21
|
export const EXTERNAL_CLIENT_ID = '__external__';
|
|
21
22
|
|
|
22
|
-
function toDialectJsonValue(
|
|
23
|
-
dialect: ServerSyncDialect,
|
|
24
|
-
value: unknown
|
|
25
|
-
): unknown {
|
|
26
|
-
if (value === null || value === undefined) return null;
|
|
27
|
-
if (dialect.family === 'sqlite') return JSON.stringify(value);
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function coerceNumber(value: unknown): number | null {
|
|
32
|
-
if (value === null || value === undefined) return null;
|
|
33
|
-
if (typeof value === 'number') {
|
|
34
|
-
return Number.isFinite(value) ? value : null;
|
|
35
|
-
}
|
|
36
|
-
if (typeof value === 'bigint') {
|
|
37
|
-
const coerced = Number(value);
|
|
38
|
-
return Number.isFinite(coerced) ? coerced : null;
|
|
39
|
-
}
|
|
40
|
-
if (typeof value === 'string') {
|
|
41
|
-
const coerced = Number(value);
|
|
42
|
-
return Number.isFinite(coerced) ? coerced : null;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
23
|
export interface NotifyExternalDataChangeArgs<DB extends SyncCoreDb> {
|
|
48
24
|
db: Kysely<DB>;
|
|
49
25
|
dialect: ServerSyncDialect;
|
|
@@ -80,8 +56,11 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
|
|
|
80
56
|
const { db, dialect, tables } = args;
|
|
81
57
|
const partitionId = args.partitionId ?? 'default';
|
|
82
58
|
const actorId = args.actorId ?? EXTERNAL_CLIENT_ID;
|
|
59
|
+
const uniqueTables = Array.from(
|
|
60
|
+
new Set(tables.filter((table) => typeof table === 'string'))
|
|
61
|
+
);
|
|
83
62
|
|
|
84
|
-
if (
|
|
63
|
+
if (uniqueTables.length === 0) {
|
|
85
64
|
throw new Error('notifyExternalDataChange: tables must not be empty');
|
|
86
65
|
}
|
|
87
66
|
|
|
@@ -103,7 +82,7 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
|
|
|
103
82
|
meta: null,
|
|
104
83
|
result_json: toDialectJsonValue(dialect, { ok: true, status: 'applied' }),
|
|
105
84
|
change_count: 0,
|
|
106
|
-
affected_tables: dialect.arrayToDb(
|
|
85
|
+
affected_tables: dialect.arrayToDb(uniqueTables) as string[],
|
|
107
86
|
};
|
|
108
87
|
|
|
109
88
|
let commitSeq = 0;
|
|
@@ -136,7 +115,7 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
|
|
|
136
115
|
|
|
137
116
|
// 2. Insert sync_table_commits entries for each affected table
|
|
138
117
|
const tableCommits: Array<Insertable<SyncCoreDb['sync_table_commits']>> =
|
|
139
|
-
|
|
118
|
+
uniqueTables.map((table) => ({
|
|
140
119
|
partition_id: partitionId,
|
|
141
120
|
table,
|
|
142
121
|
commit_seq: commitSeq,
|
|
@@ -150,21 +129,17 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
|
|
|
150
129
|
)
|
|
151
130
|
.execute();
|
|
152
131
|
|
|
153
|
-
// 3. Delete cached snapshot chunks for affected tables
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.executeTakeFirst();
|
|
161
|
-
|
|
162
|
-
deletedChunks += Number(result?.numDeletedRows ?? 0);
|
|
163
|
-
}
|
|
132
|
+
// 3. Delete cached snapshot chunks for affected tables.
|
|
133
|
+
const deletedResult = await syncTrx
|
|
134
|
+
.deleteFrom('sync_snapshot_chunks')
|
|
135
|
+
.where('partition_id', '=', partitionId)
|
|
136
|
+
.where('scope', 'in', uniqueTables)
|
|
137
|
+
.executeTakeFirst();
|
|
138
|
+
const deletedChunks = Number(deletedResult?.numDeletedRows ?? 0);
|
|
164
139
|
|
|
165
140
|
return {
|
|
166
141
|
commitSeq,
|
|
167
|
-
tables,
|
|
142
|
+
tables: uniqueTables,
|
|
168
143
|
deletedChunks,
|
|
169
144
|
};
|
|
170
145
|
});
|
package/src/proxy/collection.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTableLookup } from '@syncular/core';
|
|
1
2
|
import type { ProxyTableHandler } from './types';
|
|
2
3
|
|
|
3
4
|
export interface ProxyHandlerCollection {
|
|
@@ -8,32 +9,9 @@ export interface ProxyHandlerCollection {
|
|
|
8
9
|
export function createProxyHandlerCollection(
|
|
9
10
|
handlers: ProxyTableHandler[]
|
|
10
11
|
): ProxyHandlerCollection {
|
|
11
|
-
const byTable =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`Proxy table handler already registered: ${handler.table}`
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
byTable.set(handler.table, handler);
|
|
19
|
-
}
|
|
12
|
+
const byTable = createTableLookup(
|
|
13
|
+
handlers,
|
|
14
|
+
(table) => `Proxy table handler already registered: ${table}`
|
|
15
|
+
);
|
|
20
16
|
return { handlers, byTable };
|
|
21
17
|
}
|
|
22
|
-
|
|
23
|
-
export function getProxyHandler(
|
|
24
|
-
collection: ProxyHandlerCollection,
|
|
25
|
-
tableName: string
|
|
26
|
-
): ProxyTableHandler | undefined {
|
|
27
|
-
return collection.byTable.get(tableName);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function getProxyHandlerOrThrow(
|
|
31
|
-
collection: ProxyHandlerCollection,
|
|
32
|
-
tableName: string
|
|
33
|
-
): ProxyTableHandler {
|
|
34
|
-
const handler = collection.byTable.get(tableName);
|
|
35
|
-
if (!handler) {
|
|
36
|
-
throw new Error(`No proxy table handler for table: ${tableName}`);
|
|
37
|
-
}
|
|
38
|
-
return handler;
|
|
39
|
-
}
|
package/src/proxy/handler.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Kysely, RawBuilder } from 'kysely';
|
|
|
8
8
|
import { sql } from 'kysely';
|
|
9
9
|
import type { ServerSyncDialect } from '../dialect/types';
|
|
10
10
|
import type { SyncCoreDb } from '../schema';
|
|
11
|
-
import {
|
|
11
|
+
import type { ProxyHandlerCollection } from './collection';
|
|
12
12
|
import {
|
|
13
13
|
appendReturning,
|
|
14
14
|
detectMutation,
|
|
@@ -111,7 +111,7 @@ export async function executeProxyQuery<DB extends SyncCoreDb>(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// Check if this table has a registered handler
|
|
114
|
-
const handler =
|
|
114
|
+
const handler = handlers.byTable.get(mutation.tableName);
|
|
115
115
|
if (!handler) {
|
|
116
116
|
// No handler registered - execute without oplog
|
|
117
117
|
// This allows proxy operations on non-synced tables
|
package/src/proxy/index.ts
CHANGED
package/src/proxy/oplog.ts
CHANGED
|
@@ -6,19 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { randomId, type SyncOp } from '@syncular/core';
|
|
8
8
|
import { type Kysely, sql } from 'kysely';
|
|
9
|
+
import { toDialectJsonValue } from '../dialect/helpers';
|
|
9
10
|
import type { ServerSyncDialect } from '../dialect/types';
|
|
10
11
|
import type { SyncCoreDb } from '../schema';
|
|
11
12
|
import type { ProxyTableHandler } from './types';
|
|
12
13
|
|
|
13
|
-
function toDialectJsonValue(
|
|
14
|
-
dialect: ServerSyncDialect,
|
|
15
|
-
value: unknown
|
|
16
|
-
): unknown {
|
|
17
|
-
if (value === null || value === undefined) return null;
|
|
18
|
-
if (dialect.family === 'sqlite') return JSON.stringify(value);
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
14
|
/**
|
|
23
15
|
* Create oplog entries for affected rows.
|
|
24
16
|
*
|
package/src/prune.ts
CHANGED
|
@@ -18,22 +18,11 @@ import type {
|
|
|
18
18
|
SqlBool,
|
|
19
19
|
} from 'kysely';
|
|
20
20
|
import { sql } from 'kysely';
|
|
21
|
+
import { coerceNumber } from './dialect/helpers';
|
|
21
22
|
import type { SyncCoreDb } from './schema';
|
|
22
23
|
|
|
23
24
|
type EmptySelection = Record<string, never>;
|
|
24
25
|
|
|
25
|
-
function coerceNumber(value: unknown): number | null {
|
|
26
|
-
if (value === null || value === undefined) return null;
|
|
27
|
-
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
28
|
-
if (typeof value === 'bigint')
|
|
29
|
-
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
30
|
-
if (typeof value === 'string') {
|
|
31
|
-
const n = Number(value);
|
|
32
|
-
return Number.isFinite(n) ? n : null;
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
export interface PruneOptions {
|
|
38
27
|
/** Clients with updated_at older than this are ignored for watermark. Default: 14 days. */
|
|
39
28
|
activeWindowMs?: number;
|