@syncular/typegen 0.1.3 → 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
package/src/emit.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * TS emitter: neutral IR → one standalone generated module exporting
3
+ *
4
+ * - `schema` — a ServerSchema-compatible object (structurally typed; the
5
+ * generated file imports nothing),
6
+ * - a `<Table>Row` interface per table,
7
+ * - `<Table>Insert` / `<Table>Update` mutation-input shapes honoring
8
+ * nullability,
9
+ * - a `<name>Subscription` helper constant per manifest subscription with
10
+ * a typed requested-scopes builder.
11
+ *
12
+ * The header carries the IR hash so `--check` verifies freshness
13
+ * byte-exactly.
14
+ */
15
+ import type { IrColumnType, IrDocument, IrSubscription, IrTable } from './ir';
16
+
17
+ const TS_TYPE: Readonly<Record<IrColumnType, string>> = {
18
+ string: 'string',
19
+ integer: 'number',
20
+ float: 'number',
21
+ boolean: 'boolean',
22
+ json: 'string',
23
+ bytes: 'Uint8Array',
24
+ // §5.9: a blob_ref value is the raw canonical BlobRef JSON string
25
+ // (like `json`); the client's blob API parses it into a BlobRef object.
26
+ blob_ref: 'string',
27
+ // §5.10: a crdt value is opaque bytes (like `bytes`); the Y.Doc accessor
28
+ // is an app-level helper in @syncular/crdt-yjs, not generated code.
29
+ crdt: 'Uint8Array',
30
+ };
31
+
32
+ /**
33
+ * §5.11: the app-side TS type of a column. An encrypted column's wire type is
34
+ * `bytes`, but the app sees its declared type (the local mirror is plaintext).
35
+ */
36
+ function appTsType(column: IrTable['columns'][number]): string {
37
+ const type =
38
+ column.encrypted === true && column.declaredType !== undefined
39
+ ? column.declaredType
40
+ : column.type;
41
+ return TS_TYPE[type];
42
+ }
43
+
44
+ function pascalCase(name: string): string {
45
+ return name
46
+ .split(/[_-]+/)
47
+ .filter((part) => part.length > 0)
48
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
49
+ .join('');
50
+ }
51
+
52
+ function quote(value: string): string {
53
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
54
+ }
55
+
56
+ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
57
+
58
+ function propertyKey(name: string): string {
59
+ return IDENTIFIER_RE.test(name) ? name : quote(name);
60
+ }
61
+
62
+ function emitSchema(ir: IrDocument): string {
63
+ const lines: string[] = [];
64
+ lines.push('/** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */');
65
+ lines.push('export const schema = {');
66
+ lines.push(` version: ${ir.schemaVersion},`);
67
+ lines.push(' tables: [');
68
+ for (const table of ir.tables) {
69
+ lines.push(' {');
70
+ lines.push(` name: ${quote(table.name)},`);
71
+ lines.push(' columns: [');
72
+ for (const column of table.columns) {
73
+ // §5.10.1: emit crdtType on crdt columns so a generated server schema
74
+ // can select the merger; omitted (and thus unchanged) for others.
75
+ const crdtType =
76
+ column.crdtType !== undefined
77
+ ? `, crdtType: ${quote(column.crdtType)}`
78
+ : '';
79
+ // §5.11: emit encrypted/declaredType on encrypted columns so the client
80
+ // codec encrypts/decrypts them; omitted for others.
81
+ const encrypted =
82
+ column.encrypted === true
83
+ ? `, encrypted: true, declaredType: ${quote(column.declaredType ?? column.type)}`
84
+ : '';
85
+ lines.push(
86
+ ` { name: ${quote(column.name)}, type: ${quote(column.type)}, nullable: ${column.nullable}${crdtType}${encrypted} },`,
87
+ );
88
+ }
89
+ lines.push(' ],');
90
+ lines.push(` primaryKey: ${quote(table.primaryKey)},`);
91
+ lines.push(' scopes: [');
92
+ for (const scope of table.scopes) {
93
+ lines.push(
94
+ ` { pattern: ${quote(scope.pattern)}, column: ${quote(scope.column)} },`,
95
+ );
96
+ }
97
+ lines.push(' ],');
98
+ // Local secondary indexes — emitted only when the table declares any, so
99
+ // index-free tables keep their exact pre-index generated bytes.
100
+ if (table.indexes.length > 0) {
101
+ lines.push(' indexes: [');
102
+ for (const index of table.indexes) {
103
+ const cols = index.columns.map((c) => quote(c)).join(', ');
104
+ lines.push(
105
+ ` { name: ${quote(index.name)}, columns: [${cols}], unique: ${index.unique} },`,
106
+ );
107
+ }
108
+ lines.push(' ],');
109
+ }
110
+ lines.push(' },');
111
+ }
112
+ lines.push(' ],');
113
+ lines.push('} as const;');
114
+ return lines.join('\n');
115
+ }
116
+
117
+ function emitRowInterfaces(table: IrTable): string {
118
+ const type = pascalCase(table.name);
119
+ const lines: string[] = [];
120
+ lines.push(`/** One ${table.name} row (§2.4 column order). */`);
121
+ lines.push(`export interface ${type}Row {`);
122
+ for (const column of table.columns) {
123
+ const ts = appTsType(column);
124
+ lines.push(
125
+ ` ${propertyKey(column.name)}: ${ts}${column.nullable ? ' | null' : ''};`,
126
+ );
127
+ }
128
+ lines.push('}');
129
+ lines.push('');
130
+ lines.push(`/** Insert shape: nullable columns may be omitted. */`);
131
+ lines.push(`export interface ${type}Insert {`);
132
+ for (const column of table.columns) {
133
+ const ts = appTsType(column);
134
+ lines.push(
135
+ column.nullable
136
+ ? ` ${propertyKey(column.name)}?: ${ts} | null;`
137
+ : ` ${propertyKey(column.name)}: ${ts};`,
138
+ );
139
+ }
140
+ lines.push('}');
141
+ lines.push('');
142
+ lines.push(
143
+ `/** Update shape: primary key required, all other columns optional. */`,
144
+ );
145
+ lines.push(`export interface ${type}Update {`);
146
+ for (const column of table.columns) {
147
+ const ts = appTsType(column);
148
+ if (column.name === table.primaryKey) {
149
+ lines.push(` ${propertyKey(column.name)}: ${ts};`);
150
+ } else {
151
+ lines.push(
152
+ ` ${propertyKey(column.name)}?: ${ts}${column.nullable ? ' | null' : ''};`,
153
+ );
154
+ }
155
+ }
156
+ lines.push('}');
157
+ return lines.join('\n');
158
+ }
159
+
160
+ /**
161
+ * A Kysely-compatible `Database` interface: table name → Row type. This is
162
+ * the generic `@syncular/kysely`'s `SyncularDialect` is parameterized by
163
+ * (`new Kysely<Database>({ dialect })`). Additive — the Row interfaces it
164
+ * references are already emitted above. Property keys use the raw table name
165
+ * (quoted when not an identifier) so `db.selectFrom('table-name')` types.
166
+ */
167
+ function emitDatabaseInterface(ir: IrDocument): string {
168
+ const lines: string[] = [];
169
+ lines.push('/** Kysely `Database` interface (table → Row); the generic for');
170
+ lines.push(" * @syncular/kysely's SyncularDialect. */");
171
+ lines.push('export interface Database {');
172
+ for (const table of ir.tables) {
173
+ lines.push(` ${propertyKey(table.name)}: ${pascalCase(table.name)}Row;`);
174
+ }
175
+ lines.push('}');
176
+ return lines.join('\n');
177
+ }
178
+
179
+ function emitSubscription(sub: IrSubscription): string {
180
+ const params: string[] = [];
181
+ for (const scope of sub.scopes) {
182
+ for (const value of scope.values) {
183
+ if (value.kind === 'parameter' && !params.includes(value.name)) {
184
+ params.push(value.name);
185
+ }
186
+ }
187
+ }
188
+ const lines: string[] = [];
189
+ const paramsType = `${pascalCase(sub.name)}Params`;
190
+ if (params.length > 0) {
191
+ lines.push(`export interface ${paramsType} {`);
192
+ for (const param of params) {
193
+ lines.push(` ${propertyKey(param)}: string;`);
194
+ }
195
+ lines.push('}');
196
+ lines.push('');
197
+ }
198
+ lines.push(
199
+ `/** Requested-scope template for the ${quote(sub.name)} subscription. */`,
200
+ );
201
+ lines.push(`export const ${sub.name}Subscription = {`);
202
+ lines.push(` name: ${quote(sub.name)},`);
203
+ lines.push(` table: ${quote(sub.table)},`);
204
+ const args = params.length > 0 ? `params: ${paramsType}` : '';
205
+ lines.push(` scopes: (${args}): Record<string, string[]> => ({`);
206
+ for (const scope of sub.scopes) {
207
+ const values = scope.values
208
+ .map((value) =>
209
+ value.kind === 'literal'
210
+ ? quote(value.value)
211
+ : `params.${propertyKey(value.name)}`,
212
+ )
213
+ .join(', ');
214
+ lines.push(` ${propertyKey(scope.variable)}: [${values}],`);
215
+ }
216
+ lines.push(' }),');
217
+ lines.push('} as const;');
218
+ return lines.join('\n');
219
+ }
220
+
221
+ export function emitModule(ir: IrDocument, hash: string): string {
222
+ const parts: string[] = [];
223
+ parts.push(
224
+ [
225
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
226
+ `// irVersion: ${ir.irVersion}`,
227
+ `// irHash: ${hash}`,
228
+ ].join('\n'),
229
+ );
230
+ parts.push(emitSchema(ir));
231
+ for (const table of ir.tables) {
232
+ parts.push(emitRowInterfaces(table));
233
+ }
234
+ parts.push(emitDatabaseInterface(ir));
235
+ for (const sub of ir.subscriptions) {
236
+ parts.push(emitSubscription(sub));
237
+ }
238
+ return `${parts.join('\n\n')}\n`;
239
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Fail-loud codegen error: every message names the offending construct. */
2
+ export class TypegenError extends Error {
3
+ /** Input the error came from (a migration file, the manifest, …). */
4
+ readonly source: string;
5
+
6
+ constructor(source: string, message: string) {
7
+ super(`${source}: ${message}`);
8
+ this.name = 'TypegenError';
9
+ this.source = source;
10
+ }
11
+ }