@syncular/typegen 0.1.2 → 0.2.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 (99) hide show
  1. package/README.md +436 -94
  2. package/dist/cli.d.ts +1 -2
  3. package/dist/cli.js +159 -73
  4. package/dist/emit-dart.d.ts +17 -0
  5. package/dist/emit-dart.js +201 -0
  6. package/dist/emit-kotlin.d.ts +16 -0
  7. package/dist/emit-kotlin.js +201 -0
  8. package/dist/emit-queries-dart.d.ts +2 -0
  9. package/dist/emit-queries-dart.js +160 -0
  10. package/dist/emit-queries-kotlin.d.ts +2 -0
  11. package/dist/emit-queries-kotlin.js +162 -0
  12. package/dist/emit-queries-swift.d.ts +2 -0
  13. package/dist/emit-queries-swift.js +146 -0
  14. package/dist/emit-queries.d.ts +2 -0
  15. package/dist/emit-queries.js +129 -0
  16. package/dist/emit-swift.d.ts +18 -0
  17. package/dist/emit-swift.js +235 -0
  18. package/dist/emit.d.ts +16 -0
  19. package/dist/emit.js +191 -0
  20. package/dist/errors.d.ts +6 -0
  21. package/dist/errors.js +10 -0
  22. package/dist/generate.d.ts +53 -18
  23. package/dist/generate.js +391 -77
  24. package/dist/index.d.ts +17 -14
  25. package/dist/index.js +16 -13
  26. package/dist/init.d.ts +12 -0
  27. package/dist/init.js +85 -0
  28. package/dist/ir.d.ts +90 -0
  29. package/dist/ir.js +94 -0
  30. package/dist/manifest.d.ts +76 -0
  31. package/dist/manifest.js +303 -0
  32. package/dist/query.d.ts +96 -0
  33. package/dist/query.js +719 -0
  34. package/dist/sql.d.ts +13 -0
  35. package/dist/sql.js +440 -0
  36. package/package.json +14 -34
  37. package/src/cli.ts +161 -82
  38. package/src/emit-dart.ts +245 -0
  39. package/src/emit-kotlin.ts +257 -0
  40. package/src/emit-queries-dart.ts +203 -0
  41. package/src/emit-queries-kotlin.ts +209 -0
  42. package/src/emit-queries-swift.ts +198 -0
  43. package/src/emit-queries.ts +183 -0
  44. package/src/emit-swift.ts +295 -0
  45. package/src/emit.ts +239 -0
  46. package/src/errors.ts +11 -0
  47. package/src/generate.ts +574 -105
  48. package/src/index.ts +16 -13
  49. package/src/init.ts +100 -0
  50. package/src/ir.ts +197 -0
  51. package/src/manifest.ts +445 -0
  52. package/src/query.ts +918 -0
  53. package/src/sql.ts +533 -0
  54. package/dist/app-contract.d.ts +0 -154
  55. package/dist/app-contract.d.ts.map +0 -1
  56. package/dist/app-contract.js +0 -250
  57. package/dist/app-contract.js.map +0 -1
  58. package/dist/checksums.d.ts +0 -6
  59. package/dist/checksums.d.ts.map +0 -1
  60. package/dist/checksums.js +0 -173
  61. package/dist/checksums.js.map +0 -1
  62. package/dist/cli.d.ts.map +0 -1
  63. package/dist/cli.js.map +0 -1
  64. package/dist/generate.d.ts.map +0 -1
  65. package/dist/generate.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/introspect-postgres.d.ts +0 -8
  69. package/dist/introspect-postgres.d.ts.map +0 -1
  70. package/dist/introspect-postgres.js +0 -94
  71. package/dist/introspect-postgres.js.map +0 -1
  72. package/dist/introspect-sqlite.d.ts +0 -10
  73. package/dist/introspect-sqlite.d.ts.map +0 -1
  74. package/dist/introspect-sqlite.js +0 -97
  75. package/dist/introspect-sqlite.js.map +0 -1
  76. package/dist/introspect.d.ts +0 -8
  77. package/dist/introspect.d.ts.map +0 -1
  78. package/dist/introspect.js +0 -18
  79. package/dist/introspect.js.map +0 -1
  80. package/dist/map-types.d.ts +0 -20
  81. package/dist/map-types.d.ts.map +0 -1
  82. package/dist/map-types.js +0 -151
  83. package/dist/map-types.js.map +0 -1
  84. package/dist/render.d.ts +0 -23
  85. package/dist/render.d.ts.map +0 -1
  86. package/dist/render.js +0 -140
  87. package/dist/render.js.map +0 -1
  88. package/dist/types.d.ts +0 -122
  89. package/dist/types.d.ts.map +0 -1
  90. package/dist/types.js +0 -5
  91. package/dist/types.js.map +0 -1
  92. package/src/app-contract.ts +0 -531
  93. package/src/checksums.ts +0 -257
  94. package/src/introspect-postgres.ts +0 -149
  95. package/src/introspect-sqlite.ts +0 -156
  96. package/src/introspect.ts +0 -36
  97. package/src/map-types.ts +0 -189
  98. package/src/render.ts +0 -196
  99. package/src/types.ts +0 -137
@@ -1,531 +0,0 @@
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
- }