@syncular/typegen 0.1.3 → 0.2.1

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
@@ -0,0 +1,445 @@
1
+ /**
2
+ * `syncular.json` manifest (REVISE B5) — designed here, minimal but
3
+ * forward-extensible:
4
+ *
5
+ * ```json
6
+ * {
7
+ * "manifestVersion": 1,
8
+ * "migrations": "./migrations",
9
+ * "output": { "ir": "./syncular.ir.json",
10
+ * "module": "./syncular.generated.ts",
11
+ * "swift": "./Sources/App/Syncular.generated.swift",
12
+ * "kotlin": { "path": "./Syncular.generated.kt",
13
+ * "package": "dev.example" },
14
+ * "dart": "./lib/syncular.generated.dart" },
15
+ * "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
16
+ * "tables": [{ "name": "tasks", "scopes": ["project:{project_id}"] }],
17
+ * "subscriptions": [{ "name": "projectTasks", "table": "tasks",
18
+ * "scopes": { "project_id": ["{projectId}"] } }],
19
+ * "extensions": {}
20
+ * }
21
+ * ```
22
+ *
23
+ * - `tables` is ordered: it is the handler-declared bootstrap order (§4.7).
24
+ * - `schemaVersions` is the §1.5 version history: strictly increasing
25
+ * versions, each naming the last migration it includes (`through`); the
26
+ * final entry must cover the final migration.
27
+ * - Subscription scope values are literals or whole-value `{param}`
28
+ * placeholders (partial templates are unsupported).
29
+ * - `output.ir` / `output.module` are the always-emitted TS defaults.
30
+ * `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native
31
+ * emitters — a bare string is the output path; an object carries
32
+ * language-appropriate options (Kotlin `package`/`objectName`, Swift
33
+ * `enumName`; Dart takes `path` only). This is additive within the `output`
34
+ * object (the same forward-extension shape `output.ir`/`output.module`
35
+ * already use); no `manifestVersion` bump — new *recognized* keys, not
36
+ * tolerated-unknown ones. Absent → that language is not generated (TS
37
+ * stays the default).
38
+ * - Unknown keys are hard errors everywhere (fail loud; growth happens by
39
+ * bumping `manifestVersion`), except inside `extensions`, the reserved
40
+ * WP-49 passthrough slot copied verbatim into the IR.
41
+ */
42
+ import { TypegenError } from './errors';
43
+
44
+ export const MANIFEST_FILENAME = 'syncular.json';
45
+
46
+ /** Same shorthand the server schema accepts (§3.1). */
47
+ export type ManifestScopeSpec = string | { pattern: string; column: string };
48
+
49
+ export interface ManifestTable {
50
+ readonly name: string;
51
+ readonly scopes: readonly ManifestScopeSpec[];
52
+ readonly extensions: Readonly<Record<string, unknown>>;
53
+ /**
54
+ * §5.11: columns to encrypt end-to-end. App config (not DDL): the IR flips
55
+ * each named column's wire type to `bytes` and records `encrypted` +
56
+ * `declaredType`. Absent/empty for tables with no encryption (byte-identical
57
+ * IR to before this rung). Hard errors (encrypted scope/crdt/pk column, or
58
+ * an unknown column) are enforced at generate time.
59
+ */
60
+ readonly encryptedColumns: readonly string[];
61
+ }
62
+
63
+ export interface ManifestSubscription {
64
+ readonly name: string;
65
+ readonly table: string;
66
+ readonly scopes: Readonly<Record<string, readonly string[]>>;
67
+ }
68
+
69
+ export interface ManifestSchemaVersion {
70
+ readonly version: number;
71
+ readonly through: string;
72
+ }
73
+
74
+ /** Swift emitter output: a `.swift` path plus optional options. */
75
+ export interface SwiftOutput {
76
+ readonly path: string;
77
+ /** The generated enum namespace (default `SyncularSchema`). */
78
+ readonly enumName: string;
79
+ /** Opt-in named-queries output path (a sibling `.swift` file). */
80
+ readonly queriesPath?: string;
81
+ }
82
+
83
+ /** Kotlin emitter output: a `.kt` path plus its package declaration. */
84
+ export interface KotlinOutput {
85
+ readonly path: string;
86
+ /** The Kotlin package declaration (default `syncular.generated`). */
87
+ readonly package: string;
88
+ /** The generated top-level object name (default `SyncularSchema`). */
89
+ readonly objectName: string;
90
+ /** Opt-in named-queries output path (a sibling `.kt` file). */
91
+ readonly queriesPath?: string;
92
+ }
93
+
94
+ /** Dart emitter output: a `.dart` path plus optional options. */
95
+ export interface DartOutput {
96
+ readonly path: string;
97
+ /** Opt-in named-queries output path (a sibling `.dart` file). */
98
+ readonly queriesPath?: string;
99
+ }
100
+
101
+ export interface ManifestOutput {
102
+ readonly ir: string;
103
+ readonly module: string;
104
+ /** Opt-in TS named-queries output path (a sibling `.ts` file). Undefined →
105
+ * named queries are not generated for TS. */
106
+ readonly queries?: string;
107
+ /** Opt-in native emitters; undefined → that language is not generated. */
108
+ readonly swift?: SwiftOutput;
109
+ readonly kotlin?: KotlinOutput;
110
+ readonly dart?: DartOutput;
111
+ }
112
+
113
+ export interface Manifest {
114
+ readonly manifestVersion: 1;
115
+ readonly migrations: string;
116
+ /** Directory of `.sql` named-query files (default `./queries`). Only read
117
+ * when at least one output requests a named-queries file. */
118
+ readonly queries: string;
119
+ readonly output: ManifestOutput;
120
+ readonly schemaVersions: readonly ManifestSchemaVersion[];
121
+ readonly tables: readonly ManifestTable[];
122
+ readonly subscriptions: readonly ManifestSubscription[];
123
+ readonly extensions: Readonly<Record<string, unknown>>;
124
+ }
125
+
126
+ const SOURCE = MANIFEST_FILENAME;
127
+
128
+ function fail(message: string): never {
129
+ throw new TypegenError(SOURCE, message);
130
+ }
131
+
132
+ function asObject(value: unknown, context: string): Record<string, unknown> {
133
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
134
+ fail(`${context} must be an object`);
135
+ }
136
+ return value as Record<string, unknown>;
137
+ }
138
+
139
+ function asString(value: unknown, context: string): string {
140
+ if (typeof value !== 'string' || value.length === 0) {
141
+ fail(`${context} must be a non-empty string`);
142
+ }
143
+ return value;
144
+ }
145
+
146
+ function rejectUnknownKeys(
147
+ obj: Record<string, unknown>,
148
+ allowed: readonly string[],
149
+ context: string,
150
+ ): void {
151
+ for (const key of Object.keys(obj)) {
152
+ if (!allowed.includes(key)) {
153
+ fail(`${context} has unknown key ${JSON.stringify(key)}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ function parseExtensions(
159
+ value: unknown,
160
+ context: string,
161
+ ): Record<string, unknown> {
162
+ if (value === undefined) return {};
163
+ return asObject(value, `${context}.extensions`);
164
+ }
165
+
166
+ function parseScopeSpec(value: unknown, context: string): ManifestScopeSpec {
167
+ if (typeof value === 'string') return value;
168
+ const obj = asObject(value, context);
169
+ rejectUnknownKeys(obj, ['pattern', 'column'], context);
170
+ return {
171
+ pattern: asString(obj.pattern, `${context}.pattern`),
172
+ column: asString(obj.column, `${context}.column`),
173
+ };
174
+ }
175
+
176
+ /** Parse a native-emitter output spec: a bare path string, or an object with
177
+ * `path` + language options. Returns the resolved output or throws. */
178
+ function parseSwiftOutput(value: unknown): SwiftOutput {
179
+ if (typeof value === 'string') {
180
+ return {
181
+ path: asString(value, 'output.swift'),
182
+ enumName: 'SyncularSchema',
183
+ };
184
+ }
185
+ const obj = asObject(value, 'output.swift');
186
+ rejectUnknownKeys(obj, ['path', 'enumName', 'queriesPath'], 'output.swift');
187
+ return {
188
+ path: asString(obj.path, 'output.swift.path'),
189
+ enumName:
190
+ obj.enumName === undefined
191
+ ? 'SyncularSchema'
192
+ : asString(obj.enumName, 'output.swift.enumName'),
193
+ ...(obj.queriesPath !== undefined
194
+ ? { queriesPath: asString(obj.queriesPath, 'output.swift.queriesPath') }
195
+ : {}),
196
+ };
197
+ }
198
+
199
+ function parseKotlinOutput(value: unknown): KotlinOutput {
200
+ if (typeof value === 'string') {
201
+ return {
202
+ path: asString(value, 'output.kotlin'),
203
+ package: 'syncular.generated',
204
+ objectName: 'SyncularSchema',
205
+ };
206
+ }
207
+ const obj = asObject(value, 'output.kotlin');
208
+ rejectUnknownKeys(
209
+ obj,
210
+ ['path', 'package', 'objectName', 'queriesPath'],
211
+ 'output.kotlin',
212
+ );
213
+ return {
214
+ path: asString(obj.path, 'output.kotlin.path'),
215
+ package:
216
+ obj.package === undefined
217
+ ? 'syncular.generated'
218
+ : asString(obj.package, 'output.kotlin.package'),
219
+ objectName:
220
+ obj.objectName === undefined
221
+ ? 'SyncularSchema'
222
+ : asString(obj.objectName, 'output.kotlin.objectName'),
223
+ ...(obj.queriesPath !== undefined
224
+ ? { queriesPath: asString(obj.queriesPath, 'output.kotlin.queriesPath') }
225
+ : {}),
226
+ };
227
+ }
228
+
229
+ function parseDartOutput(value: unknown): DartOutput {
230
+ if (typeof value === 'string') {
231
+ return { path: asString(value, 'output.dart') };
232
+ }
233
+ const obj = asObject(value, 'output.dart');
234
+ rejectUnknownKeys(obj, ['path', 'queriesPath'], 'output.dart');
235
+ return {
236
+ path: asString(obj.path, 'output.dart.path'),
237
+ ...(obj.queriesPath !== undefined
238
+ ? { queriesPath: asString(obj.queriesPath, 'output.dart.queriesPath') }
239
+ : {}),
240
+ };
241
+ }
242
+
243
+ function parseTable(value: unknown, index: number): ManifestTable {
244
+ const context = `tables[${index}]`;
245
+ const obj = asObject(value, context);
246
+ rejectUnknownKeys(
247
+ obj,
248
+ ['name', 'scopes', 'extensions', 'encryptedColumns'],
249
+ context,
250
+ );
251
+ const name = asString(obj.name, `${context}.name`);
252
+ if (!Array.isArray(obj.scopes) || obj.scopes.length === 0) {
253
+ fail(`table ${name}: scopes must be a non-empty array (§3.1)`);
254
+ }
255
+ let encryptedColumns: string[] = [];
256
+ if (obj.encryptedColumns !== undefined) {
257
+ if (!Array.isArray(obj.encryptedColumns)) {
258
+ fail(`table ${name}: encryptedColumns must be an array of column names`);
259
+ }
260
+ encryptedColumns = obj.encryptedColumns.map((c, i) =>
261
+ asString(c, `table ${name} encryptedColumns[${i}]`),
262
+ );
263
+ const seen = new Set<string>();
264
+ for (const col of encryptedColumns) {
265
+ if (seen.has(col)) {
266
+ fail(
267
+ `table ${name}: duplicate encryptedColumns entry ${JSON.stringify(col)}`,
268
+ );
269
+ }
270
+ seen.add(col);
271
+ }
272
+ }
273
+ return {
274
+ name,
275
+ scopes: obj.scopes.map((spec, i) =>
276
+ parseScopeSpec(spec, `table ${name} scopes[${i}]`),
277
+ ),
278
+ extensions: parseExtensions(obj.extensions, `table ${name}`),
279
+ encryptedColumns,
280
+ };
281
+ }
282
+
283
+ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
284
+
285
+ function parseSubscription(
286
+ value: unknown,
287
+ index: number,
288
+ ): ManifestSubscription {
289
+ const context = `subscriptions[${index}]`;
290
+ const obj = asObject(value, context);
291
+ rejectUnknownKeys(obj, ['name', 'table', 'scopes'], context);
292
+ const name = asString(obj.name, `${context}.name`);
293
+ if (!IDENTIFIER_RE.test(name)) {
294
+ fail(
295
+ `subscription ${JSON.stringify(name)}: name must be a valid identifier`,
296
+ );
297
+ }
298
+ const table = asString(obj.table, `subscription ${name}: table`);
299
+ const scopesObj = asObject(obj.scopes, `subscription ${name}: scopes`);
300
+ const scopes: Record<string, readonly string[]> = {};
301
+ for (const [variable, values] of Object.entries(scopesObj)) {
302
+ if (!Array.isArray(values) || values.length === 0) {
303
+ fail(
304
+ `subscription ${name}: scopes.${variable} must be a non-empty array`,
305
+ );
306
+ }
307
+ scopes[variable] = values.map((v, i) =>
308
+ asString(v, `subscription ${name}: scopes.${variable}[${i}]`),
309
+ );
310
+ }
311
+ if (Object.keys(scopes).length === 0) {
312
+ fail(`subscription ${name}: scopes must not be empty`);
313
+ }
314
+ return { name, table, scopes };
315
+ }
316
+
317
+ function parseSchemaVersion(
318
+ value: unknown,
319
+ index: number,
320
+ ): ManifestSchemaVersion {
321
+ const context = `schemaVersions[${index}]`;
322
+ const obj = asObject(value, context);
323
+ rejectUnknownKeys(obj, ['version', 'through'], context);
324
+ const version = obj.version;
325
+ if (
326
+ typeof version !== 'number' ||
327
+ !Number.isInteger(version) ||
328
+ version < 1
329
+ ) {
330
+ fail(`${context}.version must be an integer ≥ 1 (§1.5)`);
331
+ }
332
+ return {
333
+ version,
334
+ through: asString(obj.through, `${context}.through`),
335
+ };
336
+ }
337
+
338
+ export function parseManifest(raw: unknown): Manifest {
339
+ const obj = asObject(raw, 'manifest');
340
+ rejectUnknownKeys(
341
+ obj,
342
+ [
343
+ 'manifestVersion',
344
+ 'migrations',
345
+ 'queries',
346
+ 'output',
347
+ 'schemaVersions',
348
+ 'tables',
349
+ 'subscriptions',
350
+ 'extensions',
351
+ ],
352
+ 'manifest',
353
+ );
354
+ if (obj.manifestVersion !== 1) {
355
+ fail(
356
+ `manifestVersion must be 1, got ${JSON.stringify(obj.manifestVersion)}`,
357
+ );
358
+ }
359
+ const migrations =
360
+ obj.migrations === undefined
361
+ ? './migrations'
362
+ : asString(obj.migrations, 'migrations');
363
+ const queries =
364
+ obj.queries === undefined ? './queries' : asString(obj.queries, 'queries');
365
+ let ir = './syncular.ir.json';
366
+ let module = './syncular.generated.ts';
367
+ let queriesOut: string | undefined;
368
+ let swift: SwiftOutput | undefined;
369
+ let kotlin: KotlinOutput | undefined;
370
+ let dart: DartOutput | undefined;
371
+ if (obj.output !== undefined) {
372
+ const output = asObject(obj.output, 'output');
373
+ rejectUnknownKeys(
374
+ output,
375
+ ['ir', 'module', 'queries', 'swift', 'kotlin', 'dart'],
376
+ 'output',
377
+ );
378
+ if (output.ir !== undefined) ir = asString(output.ir, 'output.ir');
379
+ if (output.module !== undefined) {
380
+ module = asString(output.module, 'output.module');
381
+ }
382
+ if (output.queries !== undefined) {
383
+ queriesOut = asString(output.queries, 'output.queries');
384
+ }
385
+ if (output.swift !== undefined) swift = parseSwiftOutput(output.swift);
386
+ if (output.kotlin !== undefined) kotlin = parseKotlinOutput(output.kotlin);
387
+ if (output.dart !== undefined) dart = parseDartOutput(output.dart);
388
+ }
389
+ // Build `output` with only the keys that are set — `exactOptionalPropertyTypes`
390
+ // forbids explicit `undefined` on optional properties.
391
+ const outputSpec: ManifestOutput = {
392
+ ir,
393
+ module,
394
+ ...(queriesOut !== undefined ? { queries: queriesOut } : {}),
395
+ ...(swift !== undefined ? { swift } : {}),
396
+ ...(kotlin !== undefined ? { kotlin } : {}),
397
+ ...(dart !== undefined ? { dart } : {}),
398
+ };
399
+ if (!Array.isArray(obj.schemaVersions) || obj.schemaVersions.length === 0) {
400
+ fail('schemaVersions must be a non-empty array (§1.5 version history)');
401
+ }
402
+ const schemaVersions = obj.schemaVersions.map(parseSchemaVersion);
403
+ for (let i = 1; i < schemaVersions.length; i++) {
404
+ const prev = schemaVersions[i - 1] as ManifestSchemaVersion;
405
+ const next = schemaVersions[i] as ManifestSchemaVersion;
406
+ if (next.version <= prev.version) {
407
+ fail(
408
+ `schemaVersions must be strictly increasing (${prev.version} then ${next.version})`,
409
+ );
410
+ }
411
+ }
412
+ if (!Array.isArray(obj.tables) || obj.tables.length === 0) {
413
+ fail('tables must be a non-empty array');
414
+ }
415
+ const tables = obj.tables.map(parseTable);
416
+ const tableNames = new Set<string>();
417
+ for (const table of tables) {
418
+ if (tableNames.has(table.name)) {
419
+ fail(`duplicate table ${JSON.stringify(table.name)}`);
420
+ }
421
+ tableNames.add(table.name);
422
+ }
423
+ const subscriptions = Array.isArray(obj.subscriptions)
424
+ ? obj.subscriptions.map(parseSubscription)
425
+ : obj.subscriptions === undefined
426
+ ? []
427
+ : fail('subscriptions must be an array');
428
+ const subNames = new Set<string>();
429
+ for (const sub of subscriptions) {
430
+ if (subNames.has(sub.name)) {
431
+ fail(`duplicate subscription ${JSON.stringify(sub.name)}`);
432
+ }
433
+ subNames.add(sub.name);
434
+ }
435
+ return {
436
+ manifestVersion: 1,
437
+ migrations,
438
+ queries,
439
+ output: outputSpec,
440
+ schemaVersions,
441
+ tables,
442
+ subscriptions,
443
+ extensions: parseExtensions(obj.extensions, 'manifest'),
444
+ };
445
+ }