@syncular/server 0.0.1 → 0.0.2-127

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.
Files changed (171) hide show
  1. package/README.md +25 -0
  2. package/dist/blobs/adapters/database.d.ts.map +1 -1
  3. package/dist/blobs/adapters/database.js +25 -3
  4. package/dist/blobs/adapters/database.js.map +1 -1
  5. package/dist/blobs/adapters/filesystem.d.ts +31 -0
  6. package/dist/blobs/adapters/filesystem.d.ts.map +1 -0
  7. package/dist/blobs/adapters/filesystem.js +140 -0
  8. package/dist/blobs/adapters/filesystem.js.map +1 -0
  9. package/dist/blobs/adapters/s3.d.ts +3 -2
  10. package/dist/blobs/adapters/s3.d.ts.map +1 -1
  11. package/dist/blobs/adapters/s3.js +49 -0
  12. package/dist/blobs/adapters/s3.js.map +1 -1
  13. package/dist/blobs/index.d.ts +1 -0
  14. package/dist/blobs/index.d.ts.map +1 -1
  15. package/dist/blobs/index.js +6 -5
  16. package/dist/blobs/index.js.map +1 -1
  17. package/dist/clients.d.ts +1 -0
  18. package/dist/clients.d.ts.map +1 -1
  19. package/dist/clients.js.map +1 -1
  20. package/dist/compaction.d.ts +1 -1
  21. package/dist/compaction.js +1 -1
  22. package/dist/dialect/base.d.ts +83 -0
  23. package/dist/dialect/base.d.ts.map +1 -0
  24. package/dist/dialect/base.js +144 -0
  25. package/dist/dialect/base.js.map +1 -0
  26. package/dist/dialect/helpers.d.ts +10 -0
  27. package/dist/dialect/helpers.d.ts.map +1 -0
  28. package/dist/dialect/helpers.js +59 -0
  29. package/dist/dialect/helpers.js.map +1 -0
  30. package/dist/dialect/index.d.ts +2 -0
  31. package/dist/dialect/index.d.ts.map +1 -1
  32. package/dist/dialect/index.js +3 -1
  33. package/dist/dialect/index.js.map +1 -1
  34. package/dist/dialect/types.d.ts +38 -46
  35. package/dist/dialect/types.d.ts.map +1 -1
  36. package/dist/{shapes → handlers}/create-handler.d.ts +18 -5
  37. package/dist/handlers/create-handler.d.ts.map +1 -0
  38. package/dist/{shapes → handlers}/create-handler.js +140 -43
  39. package/dist/handlers/create-handler.js.map +1 -0
  40. package/dist/handlers/index.d.ts.map +1 -0
  41. package/dist/handlers/index.js +4 -0
  42. package/dist/handlers/index.js.map +1 -0
  43. package/dist/handlers/registry.d.ts.map +1 -0
  44. package/dist/handlers/registry.js.map +1 -0
  45. package/dist/{shapes → handlers}/types.d.ts +7 -7
  46. package/dist/{shapes → handlers}/types.d.ts.map +1 -1
  47. package/dist/{shapes → handlers}/types.js.map +1 -1
  48. package/dist/helpers/conflict.d.ts +1 -1
  49. package/dist/helpers/conflict.d.ts.map +1 -1
  50. package/dist/helpers/emitted-change.d.ts +1 -1
  51. package/dist/helpers/emitted-change.d.ts.map +1 -1
  52. package/dist/helpers/index.js +4 -4
  53. package/dist/index.d.ts +2 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +17 -16
  56. package/dist/index.js.map +1 -1
  57. package/dist/notify.d.ts +47 -0
  58. package/dist/notify.d.ts.map +1 -0
  59. package/dist/notify.js +85 -0
  60. package/dist/notify.js.map +1 -0
  61. package/dist/proxy/handler.d.ts +1 -1
  62. package/dist/proxy/handler.d.ts.map +1 -1
  63. package/dist/proxy/handler.js +15 -11
  64. package/dist/proxy/handler.js.map +1 -1
  65. package/dist/proxy/index.d.ts +2 -2
  66. package/dist/proxy/index.d.ts.map +1 -1
  67. package/dist/proxy/index.js +3 -3
  68. package/dist/proxy/index.js.map +1 -1
  69. package/dist/proxy/mutation-detector.d.ts +4 -0
  70. package/dist/proxy/mutation-detector.d.ts.map +1 -1
  71. package/dist/proxy/mutation-detector.js +209 -24
  72. package/dist/proxy/mutation-detector.js.map +1 -1
  73. package/dist/proxy/oplog.d.ts +2 -1
  74. package/dist/proxy/oplog.d.ts.map +1 -1
  75. package/dist/proxy/oplog.js +15 -9
  76. package/dist/proxy/oplog.js.map +1 -1
  77. package/dist/proxy/registry.d.ts +0 -11
  78. package/dist/proxy/registry.d.ts.map +1 -1
  79. package/dist/proxy/registry.js +0 -24
  80. package/dist/proxy/registry.js.map +1 -1
  81. package/dist/proxy/types.d.ts +2 -0
  82. package/dist/proxy/types.d.ts.map +1 -1
  83. package/dist/pull.d.ts +4 -3
  84. package/dist/pull.d.ts.map +1 -1
  85. package/dist/pull.js +565 -314
  86. package/dist/pull.js.map +1 -1
  87. package/dist/push.d.ts +15 -3
  88. package/dist/push.d.ts.map +1 -1
  89. package/dist/push.js +359 -229
  90. package/dist/push.js.map +1 -1
  91. package/dist/realtime/index.js +1 -1
  92. package/dist/realtime/types.d.ts +2 -0
  93. package/dist/realtime/types.d.ts.map +1 -1
  94. package/dist/schema.d.ts +11 -1
  95. package/dist/schema.d.ts.map +1 -1
  96. package/dist/snapshot-chunks/db-metadata.d.ts +6 -1
  97. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  98. package/dist/snapshot-chunks/db-metadata.js +261 -92
  99. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  100. package/dist/snapshot-chunks/index.d.ts +0 -1
  101. package/dist/snapshot-chunks/index.d.ts.map +1 -1
  102. package/dist/snapshot-chunks/index.js +2 -3
  103. package/dist/snapshot-chunks/index.js.map +1 -1
  104. package/dist/snapshot-chunks/types.d.ts +20 -5
  105. package/dist/snapshot-chunks/types.d.ts.map +1 -1
  106. package/dist/snapshot-chunks.d.ts +12 -8
  107. package/dist/snapshot-chunks.d.ts.map +1 -1
  108. package/dist/snapshot-chunks.js +40 -12
  109. package/dist/snapshot-chunks.js.map +1 -1
  110. package/dist/subscriptions/index.js +1 -1
  111. package/dist/subscriptions/resolve.d.ts +6 -6
  112. package/dist/subscriptions/resolve.d.ts.map +1 -1
  113. package/dist/subscriptions/resolve.js +53 -14
  114. package/dist/subscriptions/resolve.js.map +1 -1
  115. package/package.json +28 -7
  116. package/src/blobs/adapters/database.test.ts +67 -0
  117. package/src/blobs/adapters/database.ts +34 -9
  118. package/src/blobs/adapters/filesystem.test.ts +132 -0
  119. package/src/blobs/adapters/filesystem.ts +189 -0
  120. package/src/blobs/adapters/s3.test.ts +522 -0
  121. package/src/blobs/adapters/s3.ts +55 -2
  122. package/src/blobs/index.ts +1 -0
  123. package/src/clients.ts +1 -0
  124. package/src/compaction.ts +1 -1
  125. package/src/dialect/base.ts +292 -0
  126. package/src/dialect/helpers.ts +61 -0
  127. package/src/dialect/index.ts +2 -0
  128. package/src/dialect/types.ts +50 -54
  129. package/src/{shapes → handlers}/create-handler.ts +219 -64
  130. package/src/{shapes → handlers}/types.ts +10 -7
  131. package/src/helpers/conflict.ts +1 -1
  132. package/src/helpers/emitted-change.ts +1 -1
  133. package/src/index.ts +2 -1
  134. package/src/notify.test.ts +516 -0
  135. package/src/notify.ts +131 -0
  136. package/src/proxy/handler.test.ts +120 -0
  137. package/src/proxy/handler.ts +18 -10
  138. package/src/proxy/index.ts +2 -1
  139. package/src/proxy/mutation-detector.test.ts +71 -0
  140. package/src/proxy/mutation-detector.ts +227 -29
  141. package/src/proxy/oplog.ts +19 -10
  142. package/src/proxy/registry.ts +0 -33
  143. package/src/proxy/types.ts +2 -0
  144. package/src/pull.ts +788 -405
  145. package/src/push.ts +507 -312
  146. package/src/realtime/types.ts +2 -0
  147. package/src/schema.ts +11 -1
  148. package/src/snapshot-chunks/db-metadata.test.ts +169 -0
  149. package/src/snapshot-chunks/db-metadata.ts +347 -105
  150. package/src/snapshot-chunks/index.ts +0 -1
  151. package/src/snapshot-chunks/types.ts +31 -5
  152. package/src/snapshot-chunks.ts +60 -21
  153. package/src/subscriptions/resolve.ts +73 -18
  154. package/dist/shapes/create-handler.d.ts.map +0 -1
  155. package/dist/shapes/create-handler.js.map +0 -1
  156. package/dist/shapes/index.d.ts.map +0 -1
  157. package/dist/shapes/index.js +0 -4
  158. package/dist/shapes/index.js.map +0 -1
  159. package/dist/shapes/registry.d.ts.map +0 -1
  160. package/dist/shapes/registry.js.map +0 -1
  161. package/dist/snapshot-chunks/adapters/s3.d.ts +0 -63
  162. package/dist/snapshot-chunks/adapters/s3.d.ts.map +0 -1
  163. package/dist/snapshot-chunks/adapters/s3.js +0 -50
  164. package/dist/snapshot-chunks/adapters/s3.js.map +0 -1
  165. package/src/snapshot-chunks/adapters/s3.ts +0 -68
  166. /package/dist/{shapes → handlers}/index.d.ts +0 -0
  167. /package/dist/{shapes → handlers}/registry.d.ts +0 -0
  168. /package/dist/{shapes → handlers}/registry.js +0 -0
  169. /package/dist/{shapes → handlers}/types.js +0 -0
  170. /package/src/{shapes → handlers}/index.ts +0 -0
  171. /package/src/{shapes → handlers}/registry.ts +0 -0
@@ -5,11 +5,20 @@
5
5
  import type {
6
6
  ScopePattern,
7
7
  ScopeValues,
8
+ ScopeValuesFromPatterns,
8
9
  ScopeDefinition as SimpleScopeDefinition,
9
10
  StoredScopes,
10
11
  SyncOperation,
11
12
  } from '@syncular/core';
12
- import { extractScopeVars, normalizeScopes } from '@syncular/core';
13
+ import {
14
+ applyCodecsFromDbRow,
15
+ applyCodecsToDbRow,
16
+ type ColumnCodecDialect,
17
+ type ColumnCodecSource,
18
+ extractScopeVars,
19
+ normalizeScopes,
20
+ toTableColumnCodecs,
21
+ } from '@syncular/core';
13
22
  import type {
14
23
  DeleteQueryBuilder,
15
24
  DeleteResult,
@@ -39,6 +48,24 @@ type AuthorizeResult =
39
48
  | true
40
49
  | { error: string; code: string; retriable?: boolean };
41
50
 
51
+ function classifyConstraintViolationCode(message: string): string {
52
+ const normalized = message.toLowerCase();
53
+ if (normalized.includes('not null')) return 'NOT_NULL_CONSTRAINT';
54
+ if (normalized.includes('unique')) return 'UNIQUE_CONSTRAINT';
55
+ if (normalized.includes('foreign key')) return 'FOREIGN_KEY_CONSTRAINT';
56
+ return 'CONSTRAINT_VIOLATION';
57
+ }
58
+
59
+ function isConstraintViolationError(message: string): boolean {
60
+ const normalized = message.toLowerCase();
61
+ return (
62
+ normalized.includes('constraint') ||
63
+ normalized.includes('not null') ||
64
+ normalized.includes('foreign key') ||
65
+ normalized.includes('unique')
66
+ );
67
+ }
68
+
42
69
  /**
43
70
  * Scope definition for a column - maps scope variable to column name.
44
71
  */
@@ -51,6 +78,8 @@ export interface CreateServerHandlerOptions<
51
78
  ServerDB extends SyncCoreDb,
52
79
  ClientDB,
53
80
  TableName extends keyof ServerDB & keyof ClientDB & string,
81
+ ScopeDefs extends
82
+ readonly SimpleScopeDefinition[] = readonly SimpleScopeDefinition[],
54
83
  > {
55
84
  /** Table name in the database */
56
85
  table: TableName;
@@ -70,7 +99,7 @@ export interface CreateServerHandlerOptions<
70
99
  * ]
71
100
  * ```
72
101
  */
73
- scopes: SimpleScopeDefinition[];
102
+ scopes: ScopeDefs;
74
103
 
75
104
  /** Primary key column name (default: 'id') */
76
105
  primaryKey?: string;
@@ -94,7 +123,9 @@ export interface CreateServerHandlerOptions<
94
123
  * project_id: await getProjectsForUser(ctx.db, ctx.actorId),
95
124
  * })
96
125
  */
97
- resolveScopes: (ctx: ServerContext<ServerDB>) => Promise<ScopeValues>;
126
+ resolveScopes: (
127
+ ctx: ServerContext<ServerDB>
128
+ ) => Promise<ScopeValuesFromPatterns<ScopeDefs>>;
98
129
 
99
130
  /**
100
131
  * Transform inbound row from client to server format.
@@ -112,6 +143,20 @@ export interface CreateServerHandlerOptions<
112
143
  row: Selectable<ServerDB[TableName]>
113
144
  ) => ClientDB[TableName];
114
145
 
146
+ /**
147
+ * Optional column codec resolver.
148
+ * Receives `{ table, column, sqlType?, dialect? }` and returns a codec.
149
+ * Only used by default snapshot/apply paths when the corresponding
150
+ * transform hook is not provided.
151
+ */
152
+ columnCodecs?: ColumnCodecSource;
153
+
154
+ /**
155
+ * Dialect used for codec dialect overrides.
156
+ * Default: 'sqlite'
157
+ */
158
+ codecDialect?: ColumnCodecDialect;
159
+
115
160
  /**
116
161
  * Authorize an operation before applying.
117
162
  * Return true to allow, or an error object to reject.
@@ -168,8 +213,10 @@ export function createServerHandler<
168
213
  ServerDB extends SyncCoreDb,
169
214
  ClientDB,
170
215
  TableName extends keyof ServerDB & keyof ClientDB & string,
216
+ ScopeDefs extends
217
+ readonly SimpleScopeDefinition[] = readonly SimpleScopeDefinition[],
171
218
  >(
172
- options: CreateServerHandlerOptions<ServerDB, ClientDB, TableName>
219
+ options: CreateServerHandlerOptions<ServerDB, ClientDB, TableName, ScopeDefs>
173
220
  ): ServerTableHandler<ServerDB> {
174
221
  type OverloadParameters<T> = T extends (...args: infer A) => unknown
175
222
  ? A
@@ -192,9 +239,25 @@ export function createServerHandler<
192
239
  resolveScopes,
193
240
  transformInbound,
194
241
  transformOutbound,
242
+ columnCodecs,
243
+ codecDialect = 'sqlite',
195
244
  authorize,
196
245
  extractScopes: customExtractScopes,
197
246
  } = options;
247
+ const codecCache = new Map<string, ReturnType<typeof toTableColumnCodecs>>();
248
+ const resolveTableCodecs = (row: Record<string, unknown>) => {
249
+ if (!columnCodecs) return {};
250
+ const columns = Object.keys(row);
251
+ if (columns.length === 0) return {};
252
+ const cacheKey = columns.slice().sort().join('\u0000');
253
+ const cached = codecCache.get(cacheKey);
254
+ if (cached) return cached;
255
+ const resolved = toTableColumnCodecs(table, columnCodecs, columns, {
256
+ dialect: codecDialect,
257
+ });
258
+ codecCache.set(cacheKey, resolved);
259
+ return resolved;
260
+ };
198
261
 
199
262
  // Normalize scopes to pattern map and extract patterns/columns
200
263
  const scopeColumnMap = normalizeScopes(scopeDefs);
@@ -234,6 +297,53 @@ export function createServerHandler<
234
297
 
235
298
  const extractScopesImpl = customExtractScopes ?? defaultExtractScopes;
236
299
 
300
+ const resolveScopesImpl = async (
301
+ ctx: ServerContext<ServerDB>
302
+ ): Promise<ScopeValues> => {
303
+ const resolved = await resolveScopes(ctx);
304
+ const normalized: ScopeValues = {};
305
+ for (const [scopeKey, scopeValue] of Object.entries(resolved)) {
306
+ if (typeof scopeValue === 'string' || Array.isArray(scopeValue)) {
307
+ normalized[scopeKey] = scopeValue;
308
+ }
309
+ }
310
+ return normalized;
311
+ };
312
+
313
+ const applyOutboundTransform = (
314
+ row: Selectable<ServerDB[TableName]>
315
+ ): ClientDB[TableName] => {
316
+ if (transformOutbound) {
317
+ return transformOutbound(row);
318
+ }
319
+
320
+ const recordRow = row as Record<string, unknown>;
321
+ const transformed = applyCodecsFromDbRow(
322
+ recordRow,
323
+ resolveTableCodecs(recordRow),
324
+ codecDialect
325
+ );
326
+ return transformed as ClientDB[TableName];
327
+ };
328
+
329
+ const applyInboundTransform = (
330
+ row: Record<string, unknown>,
331
+ schemaVersion: number | undefined
332
+ ): Updateable<ServerDB[TableName]> => {
333
+ if (transformInbound) {
334
+ return transformInbound(row as ClientDB[TableName], {
335
+ schemaVersion,
336
+ });
337
+ }
338
+
339
+ const transformed = applyCodecsToDbRow(
340
+ row,
341
+ resolveTableCodecs(row),
342
+ codecDialect
343
+ );
344
+ return transformed as Updateable<ServerDB[TableName]>;
345
+ };
346
+
237
347
  // Default snapshot implementation
238
348
  const defaultSnapshot = async (
239
349
  ctx: ServerSnapshotContext<ServerDB>,
@@ -290,11 +400,9 @@ export function createServerHandler<
290
400
  : null;
291
401
 
292
402
  // Transform outbound if provided
293
- const outputRows = transformOutbound
294
- ? pageRows.map((r) =>
295
- transformOutbound(r as Selectable<ServerDB[TableName]>)
296
- )
297
- : pageRows;
403
+ const outputRows = pageRows.map((r) =>
404
+ applyOutboundTransform(r as Selectable<ServerDB[TableName]>)
405
+ );
298
406
 
299
407
  return {
300
408
  rows: outputRows,
@@ -390,11 +498,11 @@ export function createServerHandler<
390
498
 
391
499
  // Handle upsert
392
500
  const rawPayload = op.payload ?? {};
393
- const payload = transformInbound
394
- ? transformInbound(rawPayload as ClientDB[TableName], {
395
- schemaVersion: ctx.schemaVersion,
396
- })
397
- : (rawPayload as Updateable<ServerDB[TableName]>);
501
+ const payloadRecord =
502
+ rawPayload !== null && typeof rawPayload === 'object'
503
+ ? (rawPayload as Record<string, unknown>)
504
+ : {};
505
+ const payload = applyInboundTransform(payloadRecord, ctx.schemaVersion);
398
506
 
399
507
  // Check for existing row
400
508
  const existing = await (
@@ -423,9 +531,24 @@ export function createServerHandler<
423
531
  status: 'conflict',
424
532
  message: `Version conflict: server=${existingVersion}, base=${op.base_version}`,
425
533
  server_version: existingVersion,
426
- server_row: transformOutbound
427
- ? transformOutbound(existing as Selectable<ServerDB[TableName]>)
428
- : existing,
534
+ server_row: applyOutboundTransform(
535
+ existing as Selectable<ServerDB[TableName]>
536
+ ),
537
+ },
538
+ emittedChanges: [],
539
+ };
540
+ }
541
+
542
+ // If the client provided a base version, they expected this row to exist.
543
+ // A missing row usually indicates stale local state after a server reset.
544
+ if (!existing && op.base_version != null) {
545
+ return {
546
+ result: {
547
+ opIndex,
548
+ status: 'error',
549
+ error: 'ROW_NOT_FOUND_FOR_BASE_VERSION',
550
+ code: 'ROW_MISSING',
551
+ retriable: false,
429
552
  },
430
553
  emittedChanges: [],
431
554
  };
@@ -433,69 +556,101 @@ export function createServerHandler<
433
556
 
434
557
  const nextVersion = existingVersion + 1;
435
558
 
436
- if (existing) {
437
- // Update - merge payload with existing
438
- const updateSet: Record<string, unknown> = {
439
- ...payload,
440
- [versionColumn]: nextVersion,
441
- };
442
- // Don't update primary key or scope columns
443
- delete updateSet[primaryKey];
444
- for (const col of Object.values(scopeColumns)) {
445
- delete updateSet[col];
559
+ let updated: Record<string, unknown> | undefined;
560
+ let constraintError: { message: string; code: string } | null = null;
561
+
562
+ try {
563
+ if (existing) {
564
+ // Update - merge payload with existing
565
+ const updateSet: Record<string, unknown> = {
566
+ ...payload,
567
+ [versionColumn]: nextVersion,
568
+ };
569
+ // Don't update primary key or scope columns
570
+ delete updateSet[primaryKey];
571
+ for (const col of Object.values(scopeColumns)) {
572
+ delete updateSet[col];
573
+ }
574
+
575
+ await (
576
+ trx.updateTable(table) as UpdateQueryBuilder<
577
+ ServerDB,
578
+ TableName,
579
+ TableName,
580
+ UpdateResult
581
+ >
582
+ )
583
+ .set(updateSet as UpdateSetObject)
584
+ .where(ref<string>(primaryKey), '=', op.row_id)
585
+ .execute();
586
+ } else {
587
+ // Insert
588
+ const insertValues: Record<string, unknown> = {
589
+ ...payload,
590
+ [primaryKey]: op.row_id,
591
+ [versionColumn]: 1,
592
+ };
593
+
594
+ await (
595
+ trx.insertInto(table) as InsertQueryBuilder<
596
+ ServerDB,
597
+ TableName,
598
+ InsertResult
599
+ >
600
+ )
601
+ .values(insertValues as Insertable<ServerDB[TableName]>)
602
+ .execute();
446
603
  }
447
604
 
448
- await (
449
- trx.updateTable(table) as UpdateQueryBuilder<
605
+ // Read back the updated row
606
+ updated = await (
607
+ trx.selectFrom(table).selectAll() as SelectQueryBuilder<
450
608
  ServerDB,
451
- TableName,
452
- TableName,
453
- UpdateResult
609
+ keyof ServerDB & string,
610
+ Record<string, unknown>
454
611
  >
455
612
  )
456
- .set(updateSet as UpdateSetObject)
457
613
  .where(ref<string>(primaryKey), '=', op.row_id)
458
- .execute();
459
- } else {
460
- // Insert
461
- const insertValues: Record<string, unknown> = {
462
- ...payload,
463
- [primaryKey]: op.row_id,
464
- [versionColumn]: 1,
614
+ .executeTakeFirstOrThrow();
615
+ } catch (err) {
616
+ const message = err instanceof Error ? err.message : String(err);
617
+ if (!isConstraintViolationError(message)) {
618
+ throw err;
619
+ }
620
+
621
+ constraintError = {
622
+ message,
623
+ code: classifyConstraintViolationCode(message),
465
624
  };
625
+ }
466
626
 
467
- await (
468
- trx.insertInto(table) as InsertQueryBuilder<
469
- ServerDB,
470
- TableName,
471
- InsertResult
472
- >
473
- )
474
- .values(insertValues as Insertable<ServerDB[TableName]>)
475
- .execute();
627
+ if (constraintError) {
628
+ return {
629
+ result: {
630
+ opIndex,
631
+ status: 'error',
632
+ error: constraintError.message,
633
+ code: constraintError.code,
634
+ retriable: false,
635
+ },
636
+ emittedChanges: [],
637
+ };
476
638
  }
477
639
 
478
- // Read back the updated row
479
- const updated = await (
480
- trx.selectFrom(table).selectAll() as SelectQueryBuilder<
481
- ServerDB,
482
- keyof ServerDB & string,
483
- Record<string, unknown>
484
- >
485
- )
486
- .where(ref<string>(primaryKey), '=', op.row_id)
487
- .executeTakeFirstOrThrow();
640
+ if (!updated) {
641
+ throw new Error('Updated row is missing after applyOperation');
642
+ }
488
643
 
489
- const updatedRow = updated as Record<string, unknown>;
644
+ const updatedRow = updated;
490
645
  const rowVersion = (updatedRow[versionColumn] as number) ?? 1;
491
646
 
492
647
  // Extract scopes from updated row
493
648
  const scopes = extractScopesImpl(updatedRow);
494
649
 
495
650
  // Transform outbound for emitted change
496
- const rowJson = transformOutbound
497
- ? transformOutbound(updated as Selectable<ServerDB[TableName]>)
498
- : updated;
651
+ const rowJson = applyOutboundTransform(
652
+ updated as Selectable<ServerDB[TableName]>
653
+ );
499
654
 
500
655
  const emitted: EmittedChange = {
501
656
  table,
@@ -517,7 +672,7 @@ export function createServerHandler<
517
672
  scopePatterns,
518
673
  dependsOn,
519
674
  snapshotChunkTtlMs,
520
- resolveScopes,
675
+ resolveScopes: resolveScopesImpl,
521
676
  extractScopes: extractScopesImpl,
522
677
  snapshot: options.snapshot ?? defaultSnapshot,
523
678
  applyOperation: options.applyOperation ?? defaultApplyOperation,
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ScopePattern,
3
3
  ScopeValues,
4
+ ScopeValuesForKeys,
4
5
  StoredScopes,
5
6
  SyncOp,
6
7
  SyncOperation,
@@ -50,12 +51,14 @@ export interface ServerContext<DB extends SyncCoreDb = SyncCoreDb> {
50
51
  /**
51
52
  * Context passed to snapshot method.
52
53
  */
53
- export interface ServerSnapshotContext<DB extends SyncCoreDb = SyncCoreDb>
54
- extends ServerContext<DB> {
54
+ export interface ServerSnapshotContext<
55
+ DB extends SyncCoreDb = SyncCoreDb,
56
+ ScopeKeys extends string = string,
57
+ > extends ServerContext<DB> {
55
58
  /** Database executor for the snapshot */
56
59
  db: DbExecutor<DB>;
57
60
  /** Effective scope values for this subscription */
58
- scopeValues: ScopeValues;
61
+ scopeValues: ScopeValuesForKeys<ScopeKeys>;
59
62
  /** Pagination cursor (row_id for keyset pagination) */
60
63
  cursor: string | null;
61
64
  /** Max rows to return */
@@ -120,9 +123,9 @@ interface ServerScopeConfig {
120
123
  }
121
124
 
122
125
  /**
123
- * Server shape options - configuration for a table's sync behavior.
126
+ * Server handler options - configuration for a table's sync behavior.
124
127
  */
125
- export interface ServerShapeOptions<
128
+ export interface ServerHandlerOptions<
126
129
  DB extends SyncCoreDb = SyncCoreDb,
127
130
  Scopes extends Record<ScopePattern, Record<string, string>> = Record<
128
131
  ScopePattern,
@@ -132,7 +135,7 @@ export interface ServerShapeOptions<
132
135
  Params extends ZodSchema = ZodSchema,
133
136
  > {
134
137
  /**
135
- * Scope patterns this shape uses.
138
+ * Scope patterns this handler uses.
136
139
  * Array of pattern keys from SharedScopes.
137
140
  */
138
141
  scopes: (keyof Scopes)[];
@@ -225,7 +228,7 @@ export interface ServerTableHandler<DB extends SyncCoreDb = SyncCoreDb> {
225
228
  /** Table name */
226
229
  table: string;
227
230
 
228
- /** Scope patterns used by this shape */
231
+ /** Scope patterns used by this handler */
229
232
  scopePatterns: ScopePattern[];
230
233
 
231
234
  /**
@@ -4,7 +4,7 @@
4
4
  * Helper for building conflict results in server table handlers.
5
5
  */
6
6
 
7
- import type { ApplyOperationResult } from '../shapes/types';
7
+ import type { ApplyOperationResult } from '../handlers/types';
8
8
 
9
9
  export interface BuildConflictResultArgs {
10
10
  /** Index of the operation in the batch */
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { StoredScopes } from '@syncular/core';
8
- import type { EmittedChange } from '../shapes/types';
8
+ import type { EmittedChange } from '../handlers/types';
9
9
 
10
10
  export interface CreateEmittedChangeArgs {
11
11
  /** Table name */
package/src/index.ts CHANGED
@@ -13,15 +13,16 @@ export * from './blobs';
13
13
  export * from './clients';
14
14
  export * from './compaction';
15
15
  export * from './dialect';
16
+ export * from './handlers';
16
17
  export * from './helpers';
17
18
  export * from './migrate';
19
+ export * from './notify';
18
20
  export * from './proxy';
19
21
  export * from './prune';
20
22
  export * from './pull';
21
23
  export * from './push';
22
24
  export * from './realtime';
23
25
  export * from './schema';
24
- export * from './shapes';
25
26
  export * from './snapshot-chunks';
26
27
  export type { SnapshotChunkStorage } from './snapshot-chunks/types';
27
28
  export * from './stats';