@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.
- package/README.md +436 -94
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +159 -73
- package/dist/emit-dart.d.ts +17 -0
- package/dist/emit-dart.js +201 -0
- package/dist/emit-kotlin.d.ts +16 -0
- package/dist/emit-kotlin.js +201 -0
- package/dist/emit-queries-dart.d.ts +2 -0
- package/dist/emit-queries-dart.js +160 -0
- package/dist/emit-queries-kotlin.d.ts +2 -0
- package/dist/emit-queries-kotlin.js +162 -0
- package/dist/emit-queries-swift.d.ts +2 -0
- package/dist/emit-queries-swift.js +146 -0
- package/dist/emit-queries.d.ts +2 -0
- package/dist/emit-queries.js +129 -0
- package/dist/emit-swift.d.ts +18 -0
- package/dist/emit-swift.js +235 -0
- package/dist/emit.d.ts +16 -0
- package/dist/emit.js +191 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/generate.d.ts +53 -18
- package/dist/generate.js +391 -77
- package/dist/index.d.ts +17 -14
- package/dist/index.js +16 -13
- package/dist/init.d.ts +12 -0
- package/dist/init.js +85 -0
- package/dist/ir.d.ts +90 -0
- package/dist/ir.js +94 -0
- package/dist/manifest.d.ts +76 -0
- package/dist/manifest.js +303 -0
- package/dist/query.d.ts +96 -0
- package/dist/query.js +719 -0
- package/dist/sql.d.ts +13 -0
- package/dist/sql.js +440 -0
- package/package.json +14 -34
- package/src/cli.ts +161 -82
- package/src/emit-dart.ts +245 -0
- package/src/emit-kotlin.ts +257 -0
- package/src/emit-queries-dart.ts +203 -0
- package/src/emit-queries-kotlin.ts +209 -0
- package/src/emit-queries-swift.ts +198 -0
- package/src/emit-queries.ts +183 -0
- package/src/emit-swift.ts +295 -0
- package/src/emit.ts +239 -0
- package/src/errors.ts +11 -0
- package/src/generate.ts +574 -105
- package/src/index.ts +16 -13
- package/src/init.ts +100 -0
- package/src/ir.ts +197 -0
- package/src/manifest.ts +445 -0
- package/src/query.ts +918 -0
- package/src/sql.ts +533 -0
- package/dist/app-contract.d.ts +0 -154
- package/dist/app-contract.d.ts.map +0 -1
- package/dist/app-contract.js +0 -250
- package/dist/app-contract.js.map +0 -1
- package/dist/checksums.d.ts +0 -6
- package/dist/checksums.d.ts.map +0 -1
- package/dist/checksums.js +0 -173
- package/dist/checksums.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/introspect-postgres.d.ts +0 -8
- package/dist/introspect-postgres.d.ts.map +0 -1
- package/dist/introspect-postgres.js +0 -94
- package/dist/introspect-postgres.js.map +0 -1
- package/dist/introspect-sqlite.d.ts +0 -10
- package/dist/introspect-sqlite.d.ts.map +0 -1
- package/dist/introspect-sqlite.js +0 -97
- package/dist/introspect-sqlite.js.map +0 -1
- package/dist/introspect.d.ts +0 -8
- package/dist/introspect.d.ts.map +0 -1
- package/dist/introspect.js +0 -18
- package/dist/introspect.js.map +0 -1
- package/dist/map-types.d.ts +0 -20
- package/dist/map-types.d.ts.map +0 -1
- package/dist/map-types.js +0 -151
- package/dist/map-types.js.map +0 -1
- package/dist/render.d.ts +0 -23
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -140
- package/dist/render.js.map +0 -1
- package/dist/types.d.ts +0 -122
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/app-contract.ts +0 -531
- package/src/checksums.ts +0 -257
- package/src/introspect-postgres.ts +0 -149
- package/src/introspect-sqlite.ts +0 -156
- package/src/introspect.ts +0 -36
- package/src/map-types.ts +0 -189
- package/src/render.ts +0 -196
- 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
|
+
}
|