@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.
- 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/manifest.ts
ADDED
|
@@ -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
|
+
}
|