@syncular/typegen 0.0.6-95 → 0.1.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.
Files changed (45) hide show
  1. package/README.md +70 -1
  2. package/dist/app-contract.d.ts +154 -0
  3. package/dist/app-contract.d.ts.map +1 -0
  4. package/dist/app-contract.js +250 -0
  5. package/dist/app-contract.js.map +1 -0
  6. package/dist/checksums.d.ts +6 -0
  7. package/dist/checksums.d.ts.map +1 -0
  8. package/dist/checksums.js +173 -0
  9. package/dist/checksums.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +88 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/generate.d.ts +0 -1
  15. package/dist/generate.d.ts.map +1 -1
  16. package/dist/generate.js +4 -7
  17. package/dist/generate.js.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/introspect-postgres.js +7 -5
  23. package/dist/introspect-postgres.js.map +1 -1
  24. package/dist/introspect-sqlite.d.ts.map +1 -1
  25. package/dist/introspect-sqlite.js +2 -1
  26. package/dist/introspect-sqlite.js.map +1 -1
  27. package/dist/introspect.js +2 -2
  28. package/dist/introspect.js.map +1 -1
  29. package/dist/map-types.js.map +1 -1
  30. package/dist/render.d.ts +1 -5
  31. package/dist/render.d.ts.map +1 -1
  32. package/dist/render.js +2 -21
  33. package/dist/render.js.map +1 -1
  34. package/dist/types.d.ts +18 -13
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +16 -6
  37. package/src/app-contract.ts +531 -0
  38. package/src/checksums.ts +257 -0
  39. package/src/cli.ts +104 -0
  40. package/src/generate.ts +0 -5
  41. package/src/index.ts +2 -0
  42. package/src/introspect-postgres.ts +7 -7
  43. package/src/introspect-sqlite.ts +6 -1
  44. package/src/render.ts +3 -43
  45. package/src/types.ts +20 -17
package/dist/types.d.ts CHANGED
@@ -3,10 +3,6 @@
3
3
  */
4
4
  import type { DefinedMigrations } from '@syncular/migrations';
5
5
  export type TypegenDialect = 'sqlite' | 'postgres';
6
- export type SyncularImportType = 'scoped' | 'umbrella' | {
7
- client: string;
8
- [packageName: string]: string;
9
- };
10
6
  /**
11
7
  * Column information for a schema column.
12
8
  */
@@ -82,15 +78,6 @@ export interface GenerateTypesOptions<DB = unknown> {
82
78
  output: string;
83
79
  /** Database dialect to use for introspection (default: 'sqlite') */
84
80
  dialect?: TypegenDialect;
85
- /** Whether to extend SyncClientDb interface (adds sync infrastructure types) */
86
- extendsSyncClientDb?: boolean;
87
- /**
88
- * Controls how syncular package imports are rendered in generated output.
89
- * - 'scoped' (default): '@syncular/client'
90
- * - 'umbrella': 'syncular/client'
91
- * - object: explicit package mapping (must include `client`)
92
- */
93
- syncularImportType?: SyncularImportType;
94
81
  /** Generate versioned interfaces (ClientDbV1, ClientDbV2, etc.) */
95
82
  includeVersionHistory?: boolean;
96
83
  /** Only generate types for these tables (default: all tables) */
@@ -114,4 +101,22 @@ export interface GenerateTypesResult {
114
101
  /** Generated TypeScript code */
115
102
  code: string;
116
103
  }
104
+ export interface GenerateMigrationChecksumsOptions<DB = unknown> {
105
+ /** Defined migrations from defineMigrations() */
106
+ migrations: DefinedMigrations<DB>;
107
+ /** Output file path for generated checksums */
108
+ output: string;
109
+ /** Database dialect to use for replay (default: 'sqlite') */
110
+ dialect?: TypegenDialect;
111
+ }
112
+ export interface GenerateMigrationChecksumsResult {
113
+ /** Path to the generated file */
114
+ outputPath: string;
115
+ /** Current schema version */
116
+ currentVersion: number;
117
+ /** Number of checksums generated */
118
+ checksumCount: number;
119
+ /** Generated TypeScript code */
120
+ code: string;
121
+ }
117
122
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/typegen",
3
- "version": "0.0.6-95",
3
+ "version": "0.1.0",
4
4
  "description": "TypeScript type generator for Syncular schemas",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -27,6 +27,9 @@
27
27
  "access": "public"
28
28
  },
29
29
  "type": "module",
30
+ "bin": {
31
+ "syncular-typegen": "./src/cli.ts"
32
+ },
30
33
  "exports": {
31
34
  ".": {
32
35
  "bun": "./src/index.ts",
@@ -40,12 +43,12 @@
40
43
  "test": "bun test --pass-with-no-tests",
41
44
  "tsgo": "tsgo --noEmit",
42
45
  "build": "tsgo",
43
- "release": "bunx syncular-publish"
46
+ "release": "syncular-publish"
44
47
  },
45
48
  "dependencies": {
46
49
  "@electric-sql/pglite": "^0.3.15",
47
- "@syncular/migrations": "0.0.6-95",
48
- "better-sqlite3": "^12.6.2",
50
+ "@syncular/migrations": "workspace:*",
51
+ "better-sqlite3": "^12.8.0",
49
52
  "kysely-bun-sqlite": "^0.4.0",
50
53
  "kysely-pglite-dialect": "^1.2.0"
51
54
  },
@@ -53,12 +56,19 @@
53
56
  "kysely": "^0.28.0"
54
57
  },
55
58
  "devDependencies": {
56
- "@syncular/config": "0.0.0",
59
+ "@syncular/config": "workspace:*",
57
60
  "@types/better-sqlite3": "^7.6.13",
58
61
  "kysely": "*"
59
62
  },
60
63
  "files": [
61
64
  "dist",
62
- "src"
65
+ "src",
66
+ "!src/**/*.test.ts",
67
+ "!src/**/*.test.tsx",
68
+ "!src/**/__tests__/**",
69
+ "!dist/**/*.test.*",
70
+ "!dist/**/__tests__/**",
71
+ "!test/**",
72
+ "!tests/**"
63
73
  ]
64
74
  }
@@ -0,0 +1,531 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import type { DefinedMigrations } from '@syncular/migrations';
5
+ import { introspectCurrentSchema } from './introspect';
6
+ import type { TableSchema, TypegenDialect } from './types';
7
+
8
+ export type SyncularScopeSource = 'actorId' | 'projectId';
9
+ export type SyncularCrdtYjsKind = 'text' | 'xml-fragment' | 'prosemirror';
10
+ export type SyncularCrdtYjsSyncMode = 'server-merge' | 'encrypted-update-log';
11
+
12
+ export interface SyncularScopeDefinition {
13
+ name?: string;
14
+ column: string;
15
+ source: SyncularScopeSource;
16
+ required?: boolean;
17
+ }
18
+
19
+ export interface SyncularCrdtYjsFieldDefinition {
20
+ field?: string;
21
+ stateColumn: string;
22
+ containerKey?: string;
23
+ rowIdField?: string;
24
+ kind?: SyncularCrdtYjsKind;
25
+ syncMode?: SyncularCrdtYjsSyncMode;
26
+ }
27
+
28
+ export interface SyncularEncryptedFieldDefinition {
29
+ field: string;
30
+ scope?: string;
31
+ rowIdField?: string;
32
+ }
33
+
34
+ export interface SyncedTableDefinition {
35
+ table: string;
36
+ subscriptionId?: string;
37
+ subscriptionParams?: Record<string, unknown>;
38
+ scopes?: readonly SyncularScopeDefinition[];
39
+ serverVersion: string;
40
+ blobColumns?: readonly string[];
41
+ crdt?: Record<string, SyncularCrdtYjsFieldDefinition>;
42
+ crdtYjsFields?: readonly SyncularCrdtYjsFieldDefinition[];
43
+ encryptedFields?: readonly SyncularEncryptedFieldDefinition[];
44
+ softDelete?: string;
45
+ sqliteWithoutRowid?: boolean;
46
+ }
47
+
48
+ export interface SyncularCountByReadModelDefinition {
49
+ name: string;
50
+ kind: 'countBy';
51
+ sourceTable: string;
52
+ outputTable: string;
53
+ dimensions: readonly string[];
54
+ countColumn?: string;
55
+ }
56
+
57
+ export type SyncularLocalReadModelDefinition =
58
+ SyncularCountByReadModelDefinition;
59
+
60
+ export interface SyncularClientSchemaSupportDefinition {
61
+ minSupported?: number;
62
+ supported?: readonly number[];
63
+ }
64
+
65
+ export interface SyncularCodegenPathsDefinition {
66
+ schemaOutputPath?: string;
67
+ typescriptOutputPath?: string;
68
+ typescriptServerOutputPath?: string;
69
+ typescriptRuntimeImportPath?: string;
70
+ rustRuntimeCratePath?: string;
71
+ nativeSwiftOutputPath?: string;
72
+ nativeKotlinOutputPath?: string;
73
+ nativeAndroidKotlinOutputPath?: string;
74
+ nativeAndroidKotlinPackage?: string;
75
+ }
76
+
77
+ export interface DefineSyncularClientOptions<
78
+ Tables extends Record<string, SyncedTableDefinition>,
79
+ > extends SyncularCodegenPathsDefinition {
80
+ migrations?: unknown;
81
+ tables: Tables;
82
+ localOnlyTables?: readonly string[];
83
+ localReadModels?: readonly SyncularLocalReadModelDefinition[];
84
+ clientSchemaSupport?: SyncularClientSchemaSupportDefinition;
85
+ }
86
+
87
+ export interface SyncularClientContract<
88
+ Tables extends Record<string, SyncedTableDefinition> = Record<
89
+ string,
90
+ SyncedTableDefinition
91
+ >,
92
+ > extends DefineSyncularClientOptions<Tables> {
93
+ readonly kind: 'syncular-client-contract';
94
+ }
95
+
96
+ export interface SyncularCodegenScopeConfig {
97
+ name?: string;
98
+ column: string;
99
+ source: SyncularScopeSource;
100
+ required?: boolean;
101
+ }
102
+
103
+ export interface SyncularCodegenCrdtYjsFieldConfig {
104
+ field: string;
105
+ stateColumn: string;
106
+ containerKey?: string;
107
+ rowIdField?: string;
108
+ kind?: SyncularCrdtYjsKind;
109
+ syncMode?: SyncularCrdtYjsSyncMode;
110
+ }
111
+
112
+ export interface SyncularCodegenEncryptedFieldConfig {
113
+ field: string;
114
+ scope?: string;
115
+ rowIdField?: string;
116
+ }
117
+
118
+ export interface SyncularCodegenTableConfig {
119
+ subscriptionId?: string;
120
+ subscriptionParams?: Record<string, unknown>;
121
+ scopes?: SyncularCodegenScopeConfig[];
122
+ serverVersionColumn: string;
123
+ blobColumns?: string[];
124
+ crdtYjsFields?: SyncularCodegenCrdtYjsFieldConfig[];
125
+ encryptedFields?: SyncularCodegenEncryptedFieldConfig[];
126
+ softDeleteColumn?: string;
127
+ sqliteWithoutRowid?: boolean;
128
+ }
129
+
130
+ export interface SyncularCodegenLocalReadModelConfig {
131
+ name: string;
132
+ kind: 'countBy';
133
+ sourceTable: string;
134
+ outputTable: string;
135
+ dimensions: string[];
136
+ countColumn: string;
137
+ }
138
+
139
+ export interface SyncularCodegenConfig extends SyncularCodegenPathsDefinition {
140
+ tables: Record<string, SyncularCodegenTableConfig>;
141
+ localOnlyTables?: string[];
142
+ localReadModels?: SyncularCodegenLocalReadModelConfig[];
143
+ clientSchemaSupport?: {
144
+ minSupported?: number;
145
+ supported?: number[];
146
+ };
147
+ }
148
+
149
+ export interface ScaffoldSyncularClientContractOptions<DB = unknown>
150
+ extends SyncularCodegenPathsDefinition {
151
+ migrations: DefinedMigrations<DB>;
152
+ dialect?: TypegenDialect;
153
+ tables?: readonly string[];
154
+ scopes?: Record<string, readonly SyncularScopeDefinition[]>;
155
+ serverVersionColumn?:
156
+ | string
157
+ | Record<string, string>
158
+ | ((table: TableSchema) => string);
159
+ subscriptionId?:
160
+ | string
161
+ | Record<string, string>
162
+ | ((table: string) => string);
163
+ sqliteWithoutRowid?:
164
+ | boolean
165
+ | Record<string, boolean>
166
+ | ((table: TableSchema) => boolean);
167
+ clientSchemaSupport?: SyncularClientSchemaSupportDefinition;
168
+ }
169
+
170
+ export function defineSyncularClient<
171
+ Tables extends Record<string, SyncedTableDefinition>,
172
+ >(
173
+ options: DefineSyncularClientOptions<Tables>
174
+ ): SyncularClientContract<Tables> {
175
+ return {
176
+ ...options,
177
+ kind: 'syncular-client-contract',
178
+ };
179
+ }
180
+
181
+ export function isSyncularClientContract(
182
+ value: unknown
183
+ ): value is SyncularClientContract {
184
+ return (
185
+ typeof value === 'object' &&
186
+ value !== null &&
187
+ (value as { kind?: unknown }).kind === 'syncular-client-contract' &&
188
+ typeof (value as { tables?: unknown }).tables === 'object' &&
189
+ (value as { tables?: unknown }).tables !== null
190
+ );
191
+ }
192
+
193
+ export function syncedTable(
194
+ options: SyncedTableDefinition
195
+ ): SyncedTableDefinition {
196
+ return { ...options };
197
+ }
198
+
199
+ export function scope(
200
+ name: string,
201
+ options: {
202
+ column?: string;
203
+ source: SyncularScopeSource;
204
+ required?: boolean;
205
+ }
206
+ ): SyncularScopeDefinition {
207
+ return {
208
+ name,
209
+ column: options.column ?? name,
210
+ source: options.source,
211
+ required: options.required,
212
+ };
213
+ }
214
+
215
+ export function yjsText(
216
+ options: Omit<SyncularCrdtYjsFieldDefinition, 'kind'>
217
+ ): SyncularCrdtYjsFieldDefinition {
218
+ return { ...options, kind: 'text' };
219
+ }
220
+
221
+ export function encryptedField(
222
+ field: string,
223
+ options: Omit<SyncularEncryptedFieldDefinition, 'field'> = {}
224
+ ): SyncularEncryptedFieldDefinition {
225
+ return { field, ...options };
226
+ }
227
+
228
+ export function countByReadModel(
229
+ options: Omit<SyncularCountByReadModelDefinition, 'kind'>
230
+ ): SyncularCountByReadModelDefinition {
231
+ return { ...options, kind: 'countBy' };
232
+ }
233
+
234
+ export function toSyncularCodegenConfig(
235
+ contract: SyncularClientContract
236
+ ): SyncularCodegenConfig {
237
+ const config: SyncularCodegenConfig = {
238
+ tables: Object.fromEntries(
239
+ Object.values(contract.tables).map((table) => [
240
+ table.table,
241
+ toCodegenTable(table),
242
+ ])
243
+ ),
244
+ };
245
+
246
+ for (const key of CODEGEN_PATH_KEYS) {
247
+ const value = contract[key];
248
+ if (value !== undefined) {
249
+ config[key] = value;
250
+ }
251
+ }
252
+
253
+ if (contract.localOnlyTables && contract.localOnlyTables.length > 0) {
254
+ config.localOnlyTables = [...contract.localOnlyTables];
255
+ }
256
+
257
+ if (contract.localReadModels && contract.localReadModels.length > 0) {
258
+ config.localReadModels = contract.localReadModels.map((model) => ({
259
+ name: model.name,
260
+ kind: model.kind,
261
+ sourceTable: model.sourceTable,
262
+ outputTable: model.outputTable,
263
+ dimensions: [...model.dimensions],
264
+ countColumn: model.countColumn ?? 'row_count',
265
+ }));
266
+ }
267
+
268
+ if (contract.clientSchemaSupport) {
269
+ config.clientSchemaSupport = {
270
+ ...(contract.clientSchemaSupport.minSupported !== undefined
271
+ ? { minSupported: contract.clientSchemaSupport.minSupported }
272
+ : {}),
273
+ ...(contract.clientSchemaSupport.supported !== undefined
274
+ ? { supported: [...contract.clientSchemaSupport.supported] }
275
+ : {}),
276
+ };
277
+ }
278
+
279
+ return config;
280
+ }
281
+
282
+ export function toSyncularCodegenJson(
283
+ contract: SyncularClientContract,
284
+ space = 2
285
+ ): string {
286
+ return `${JSON.stringify(toSyncularCodegenConfig(contract), null, space)}\n`;
287
+ }
288
+
289
+ export async function writeSyncularCodegenJson(
290
+ contract: SyncularClientContract,
291
+ outputPath: string | URL = 'generated/syncular.codegen.json',
292
+ space = 2
293
+ ): Promise<void> {
294
+ if (typeof outputPath === 'string') {
295
+ await mkdir(dirname(outputPath), { recursive: true });
296
+ } else if (outputPath.protocol === 'file:') {
297
+ await mkdir(dirname(fileURLToPath(outputPath)), { recursive: true });
298
+ }
299
+ await writeFile(outputPath, toSyncularCodegenJson(contract, space));
300
+ }
301
+
302
+ export interface LoadSyncularClientContractOptions {
303
+ modulePath: string | URL;
304
+ exportName?: string;
305
+ }
306
+
307
+ export async function loadSyncularClientContract(
308
+ options: LoadSyncularClientContractOptions
309
+ ): Promise<SyncularClientContract> {
310
+ const exportName = options.exportName ?? 'app';
311
+ const module = (await import(
312
+ syncularContractModuleSpecifier(options.modulePath)
313
+ )) as Record<string, unknown>;
314
+ const contract = module[exportName];
315
+ if (!isSyncularClientContract(contract)) {
316
+ throw new Error(
317
+ `Syncular app contract module must export ${exportName} from defineSyncularClient(...)`
318
+ );
319
+ }
320
+ return contract;
321
+ }
322
+
323
+ export interface WriteSyncularCodegenJsonFromModuleOptions
324
+ extends LoadSyncularClientContractOptions {
325
+ outputPath?: string | URL;
326
+ space?: number;
327
+ }
328
+
329
+ export async function writeSyncularCodegenJsonFromModule(
330
+ options: WriteSyncularCodegenJsonFromModuleOptions
331
+ ): Promise<SyncularClientContract> {
332
+ const contract = await loadSyncularClientContract(options);
333
+ await writeSyncularCodegenJson(
334
+ contract,
335
+ options.outputPath ?? 'generated/syncular.codegen.json',
336
+ options.space
337
+ );
338
+ return contract;
339
+ }
340
+
341
+ export async function scaffoldSyncularClientContract<DB = unknown>(
342
+ options: ScaffoldSyncularClientContractOptions<DB>
343
+ ): Promise<SyncularClientContract<Record<string, SyncedTableDefinition>>> {
344
+ const schema = await introspectCurrentSchema(
345
+ options.migrations,
346
+ options.dialect ?? 'sqlite',
347
+ options.tables ? [...options.tables] : undefined
348
+ );
349
+ const tables = Object.fromEntries(
350
+ schema.tables.map((table) => {
351
+ const serverVersion = resolveServerVersionColumn(
352
+ table,
353
+ options.serverVersionColumn
354
+ );
355
+ assertColumnExists(table, serverVersion, 'server version');
356
+ const synced = syncedTable({
357
+ table: table.name,
358
+ subscriptionId: resolveStringOption(
359
+ table.name,
360
+ options.subscriptionId,
361
+ `sub-${table.name}`
362
+ ),
363
+ serverVersion,
364
+ scopes: options.scopes?.[table.name] ?? [],
365
+ sqliteWithoutRowid: resolveBooleanOption(
366
+ table,
367
+ options.sqliteWithoutRowid
368
+ ),
369
+ });
370
+ return [table.name, synced];
371
+ })
372
+ );
373
+
374
+ return defineSyncularClient({
375
+ ...pickCodegenPathOptions(options),
376
+ clientSchemaSupport: options.clientSchemaSupport,
377
+ tables,
378
+ });
379
+ }
380
+
381
+ const CODEGEN_PATH_KEYS = [
382
+ 'schemaOutputPath',
383
+ 'typescriptOutputPath',
384
+ 'typescriptServerOutputPath',
385
+ 'typescriptRuntimeImportPath',
386
+ 'rustRuntimeCratePath',
387
+ 'nativeSwiftOutputPath',
388
+ 'nativeKotlinOutputPath',
389
+ 'nativeAndroidKotlinOutputPath',
390
+ 'nativeAndroidKotlinPackage',
391
+ ] as const satisfies readonly (keyof SyncularCodegenPathsDefinition)[];
392
+
393
+ function toCodegenTable(
394
+ table: SyncedTableDefinition
395
+ ): SyncularCodegenTableConfig {
396
+ const config: SyncularCodegenTableConfig = {
397
+ serverVersionColumn: table.serverVersion,
398
+ };
399
+
400
+ if (table.subscriptionId !== undefined) {
401
+ config.subscriptionId = table.subscriptionId;
402
+ }
403
+ if (table.subscriptionParams !== undefined) {
404
+ config.subscriptionParams = table.subscriptionParams;
405
+ }
406
+ if (table.scopes && table.scopes.length > 0) {
407
+ config.scopes = table.scopes.map((item) => ({
408
+ ...(item.name !== undefined ? { name: item.name } : {}),
409
+ column: item.column,
410
+ source: item.source,
411
+ ...(item.required !== undefined ? { required: item.required } : {}),
412
+ }));
413
+ }
414
+ if (table.blobColumns && table.blobColumns.length > 0) {
415
+ config.blobColumns = [...table.blobColumns];
416
+ }
417
+
418
+ const crdtFields = [
419
+ ...Object.entries(table.crdt ?? {}).map(([field, definition]) => ({
420
+ field,
421
+ ...definition,
422
+ })),
423
+ ...(table.crdtYjsFields ?? []),
424
+ ];
425
+ if (crdtFields.length > 0) {
426
+ config.crdtYjsFields = crdtFields.map((field) => ({
427
+ field: field.field ?? '',
428
+ stateColumn: field.stateColumn,
429
+ ...(field.containerKey !== undefined
430
+ ? { containerKey: field.containerKey }
431
+ : {}),
432
+ ...(field.rowIdField !== undefined
433
+ ? { rowIdField: field.rowIdField }
434
+ : {}),
435
+ ...(field.kind !== undefined ? { kind: field.kind } : {}),
436
+ ...(field.syncMode !== undefined ? { syncMode: field.syncMode } : {}),
437
+ }));
438
+ }
439
+ if (table.encryptedFields && table.encryptedFields.length > 0) {
440
+ config.encryptedFields = table.encryptedFields.map((field) => ({
441
+ field: field.field,
442
+ ...(field.scope !== undefined ? { scope: field.scope } : {}),
443
+ ...(field.rowIdField !== undefined
444
+ ? { rowIdField: field.rowIdField }
445
+ : {}),
446
+ }));
447
+ }
448
+ if (table.softDelete !== undefined) {
449
+ config.softDeleteColumn = table.softDelete;
450
+ }
451
+ if (table.sqliteWithoutRowid !== undefined) {
452
+ config.sqliteWithoutRowid = table.sqliteWithoutRowid;
453
+ }
454
+
455
+ return config;
456
+ }
457
+
458
+ function pickCodegenPathOptions(
459
+ options: SyncularCodegenPathsDefinition
460
+ ): SyncularCodegenPathsDefinition {
461
+ const picked: SyncularCodegenPathsDefinition = {};
462
+ for (const key of CODEGEN_PATH_KEYS) {
463
+ const value = options[key];
464
+ if (value !== undefined) {
465
+ picked[key] = value;
466
+ }
467
+ }
468
+ return picked;
469
+ }
470
+
471
+ function syncularContractModuleSpecifier(modulePath: string | URL): string {
472
+ if (modulePath instanceof URL) return modulePath.href;
473
+ try {
474
+ const parsed = new URL(modulePath);
475
+ if (parsed.protocol === 'file:') return parsed.href;
476
+ } catch {
477
+ // Treat non-URL strings as filesystem paths or package specifiers below.
478
+ }
479
+ if (syncularLooksLikeLocalModulePath(modulePath)) {
480
+ return pathToFileURL(resolve(modulePath)).href;
481
+ }
482
+ return modulePath;
483
+ }
484
+
485
+ function syncularLooksLikeLocalModulePath(modulePath: string): boolean {
486
+ return (
487
+ modulePath.startsWith('.') ||
488
+ modulePath.startsWith('/') ||
489
+ (!modulePath.startsWith('@') && modulePath.includes('/')) ||
490
+ /\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$/.test(modulePath)
491
+ );
492
+ }
493
+
494
+ function resolveServerVersionColumn(
495
+ table: TableSchema,
496
+ option: ScaffoldSyncularClientContractOptions['serverVersionColumn']
497
+ ): string {
498
+ if (typeof option === 'function') return option(table);
499
+ if (typeof option === 'string') return option;
500
+ return option?.[table.name] ?? 'server_version';
501
+ }
502
+
503
+ function resolveStringOption(
504
+ table: string,
505
+ option: ScaffoldSyncularClientContractOptions['subscriptionId'],
506
+ fallback: string
507
+ ): string {
508
+ if (typeof option === 'function') return option(table);
509
+ if (typeof option === 'string') return option;
510
+ return option?.[table] ?? fallback;
511
+ }
512
+
513
+ function resolveBooleanOption(
514
+ table: TableSchema,
515
+ option: ScaffoldSyncularClientContractOptions['sqliteWithoutRowid']
516
+ ): boolean | undefined {
517
+ if (typeof option === 'function') return option(table);
518
+ if (typeof option === 'boolean') return option;
519
+ return option?.[table.name];
520
+ }
521
+
522
+ function assertColumnExists(
523
+ table: TableSchema,
524
+ column: string,
525
+ label: string
526
+ ): void {
527
+ if (table.columns.some((candidate) => candidate.name === column)) return;
528
+ throw new Error(
529
+ `Cannot scaffold Syncular table ${table.name}: ${label} column ${column} does not exist`
530
+ );
531
+ }