@syncular/typegen 0.15.12 → 0.15.14
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 +41 -7
- package/dist/emit-queries-rust.d.ts +2 -0
- package/dist/emit-queries-rust.js +748 -0
- package/dist/emit-queries.js +47 -3
- package/dist/generate.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lsp.js +2 -0
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +24 -3
- package/dist/naming.d.ts +11 -1
- package/dist/naming.js +126 -2
- package/package.json +3 -3
- package/src/emit-queries-rust.ts +975 -0
- package/src/emit-queries.ts +57 -2
- package/src/generate.ts +16 -1
- package/src/index.ts +1 -0
- package/src/lsp.ts +1 -0
- package/src/manifest.ts +37 -4
- package/src/naming.ts +153 -3
package/dist/emit-queries.js
CHANGED
|
@@ -18,6 +18,27 @@ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
|
18
18
|
function propertyKey(name) {
|
|
19
19
|
return IDENTIFIER_RE.test(name) ? name : quote(name);
|
|
20
20
|
}
|
|
21
|
+
function emitResultMapper(query, Row) {
|
|
22
|
+
const booleans = query.columns.filter((column) => column.type === 'boolean');
|
|
23
|
+
const lines = [
|
|
24
|
+
`/** Decode one storage-shaped result row for ${quote(query.name)}. */`,
|
|
25
|
+
`export function ${query.name}MapRow(row: Readonly<Record<string, unknown>>): ${Row} {`,
|
|
26
|
+
];
|
|
27
|
+
if (booleans.length === 0) {
|
|
28
|
+
lines.push(` return row as unknown as ${Row};`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
lines.push(' return {', ' ...row,');
|
|
32
|
+
for (const column of booleans) {
|
|
33
|
+
const access = `row[${quote(column.langName)}]`;
|
|
34
|
+
const decoded = `decodeQueryBoolean(${access}, ${quote(query.name)}, ${quote(column.langName)})`;
|
|
35
|
+
lines.push(` ${propertyKey(column.langName)}: ${column.nullable ? `${access} === null ? null : ${decoded}` : decoded},`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(` } as unknown as ${Row};`);
|
|
38
|
+
}
|
|
39
|
+
lines.push('}');
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
21
42
|
/** A named-query param value is the SqlValue subset its type maps to. */
|
|
22
43
|
const PARAM_TS_TYPE = TS_TYPE;
|
|
23
44
|
const SYQL_PARAM_TS_TYPE = {
|
|
@@ -161,6 +182,7 @@ function emitSyqlQuery(query, hash) {
|
|
|
161
182
|
lines.push(` ${propertyKey(column.langName)}: ${TS_TYPE[column.type]}${column.nullable ? ' | null' : ''};`);
|
|
162
183
|
}
|
|
163
184
|
lines.push('}', '');
|
|
185
|
+
lines.push(...emitResultMapper(query, Row), '');
|
|
164
186
|
for (const input of inputs) {
|
|
165
187
|
if (input.kind === 'group') {
|
|
166
188
|
lines.push(`export interface ${pascalCase(query.name)}${pascalCase(input.langName)} {`);
|
|
@@ -240,12 +262,12 @@ function emitSyqlQuery(query, hash) {
|
|
|
240
262
|
: requiresParams
|
|
241
263
|
? `, params: ${Params}`
|
|
242
264
|
: `, params?: ${Params}`;
|
|
243
|
-
lines.push(`/** Run the ${quote(query.name)} named query (SELECT-only). */`, `export async function ${query.name}(client: QueryClient${runnerParam}): Promise<${Row}[]> {`, ` const selected = ${query.name}Select(${hasParams ? 'params' : ''});`, ' const rows = await client.query(selected.sql, selected.bind);', ` return rows
|
|
265
|
+
lines.push(`/** Run the ${quote(query.name)} named query (SELECT-only). */`, `export async function ${query.name}(client: QueryClient${runnerParam}): Promise<${Row}[]> {`, ` const selected = ${query.name}Select(${hasParams ? 'params' : ''});`, ' const rows = await client.query(selected.sql, selected.bind);', ` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`, '}', '');
|
|
244
266
|
const paramsType = hasParams ? Params : 'undefined';
|
|
245
267
|
const defaultStatement = metadata.plan.statements.find((statement) => (statement.activationMask === undefined ||
|
|
246
268
|
statement.activationMask === 0) &&
|
|
247
269
|
(sort?.kind !== 'sort' || statement.sortProfile === sort.defaultProfile));
|
|
248
|
-
lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`, `export const ${query.name}Query: NamedQuery<${Row}, ${paramsType}> = {`, ` id: ${quote(`${hash}/${query.name}`)},`, ` hasParams: ${hasParams},`, ` sql: ${quote(defaultStatement?.positionalSql ?? query.positionalSql)},`);
|
|
270
|
+
lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`, `export const ${query.name}Query: NamedQuery<${Row}, ${paramsType}> = {`, ` id: ${quote(`${hash}/${query.name}`)},`, ` hasParams: ${hasParams},`, ` sql: ${quote(defaultStatement?.positionalSql ?? query.positionalSql)},`, ` mapRow: ${query.name}MapRow,`);
|
|
249
271
|
if (hasParams) {
|
|
250
272
|
lines.push(` sqlFor: (params: ${Params}) => ${query.name}Select(params).sql,`);
|
|
251
273
|
}
|
|
@@ -297,6 +319,8 @@ function emitQuery(query, hash) {
|
|
|
297
319
|
}
|
|
298
320
|
lines.push('}');
|
|
299
321
|
lines.push('');
|
|
322
|
+
lines.push(...emitResultMapper(query, Row));
|
|
323
|
+
lines.push('');
|
|
300
324
|
const hasParams = query.params.length > 0;
|
|
301
325
|
if (hasParams) {
|
|
302
326
|
lines.push(`/** Named parameters for ${quote(query.name)}. */`);
|
|
@@ -324,7 +348,7 @@ function emitQuery(query, hash) {
|
|
|
324
348
|
else {
|
|
325
349
|
lines.push(` const rows = await client.query(${sqlConst});`);
|
|
326
350
|
}
|
|
327
|
-
lines.push(` return rows
|
|
351
|
+
lines.push(` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`);
|
|
328
352
|
lines.push('}');
|
|
329
353
|
lines.push('');
|
|
330
354
|
lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`);
|
|
@@ -333,6 +357,7 @@ function emitQuery(query, hash) {
|
|
|
333
357
|
lines.push(` id: ${quote(`${hash}/${query.name}`)},`);
|
|
334
358
|
lines.push(` hasParams: ${hasParams},`);
|
|
335
359
|
lines.push(` sql: ${sqlConst},`);
|
|
360
|
+
lines.push(` mapRow: ${query.name}MapRow,`);
|
|
336
361
|
lines.push(` tables: ${query.name}Tables,`);
|
|
337
362
|
const reactiveUsesParams = query.reactive.dependencies.some((dependency) => dependency.scopes.some((scope) => scope.params.length > 0));
|
|
338
363
|
lines.push(` dependencies: (${reactiveUsesParams ? 'params' : ''}) => [`);
|
|
@@ -398,6 +423,24 @@ export function emitQueriesModule(queries, hash, irVersion) {
|
|
|
398
423
|
'}',
|
|
399
424
|
].join('\n'));
|
|
400
425
|
}
|
|
426
|
+
if (queries.some((query) => query.columns.some((column) => column.type === 'boolean'))) {
|
|
427
|
+
parts.push([
|
|
428
|
+
'/** A generated named-query row did not match its analyzed result type. */',
|
|
429
|
+
'export class QueryResultDecodeError extends TypeError {',
|
|
430
|
+
" readonly name = 'QueryResultDecodeError';",
|
|
431
|
+
' constructor(readonly query: string, readonly column: string) {',
|
|
432
|
+
" super(query + ': result column ' + column + ' is not a SQLite boolean');",
|
|
433
|
+
' }',
|
|
434
|
+
'}',
|
|
435
|
+
'',
|
|
436
|
+
'/** Lift a SQLite boolean: preserve booleans, map 0 to false and finite non-zero numbers to true. */',
|
|
437
|
+
'function decodeQueryBoolean(value: unknown, query: string, column: string): boolean {',
|
|
438
|
+
" if (typeof value === 'boolean') return value;",
|
|
439
|
+
" if (typeof value === 'number' && Number.isFinite(value)) return value !== 0;",
|
|
440
|
+
' throw new QueryResultDecodeError(query, column);',
|
|
441
|
+
'}',
|
|
442
|
+
].join('\n'));
|
|
443
|
+
}
|
|
401
444
|
parts.push([
|
|
402
445
|
"/** A bindable SQL param/row value (the wrapper's SqlValue subset). */",
|
|
403
446
|
'export type QueryValue =',
|
|
@@ -427,6 +470,7 @@ export function emitQueriesModule(queries, hash, irVersion) {
|
|
|
427
470
|
' readonly id: string;',
|
|
428
471
|
' readonly hasParams: boolean;',
|
|
429
472
|
' readonly sql: string;',
|
|
473
|
+
' readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;',
|
|
430
474
|
' readonly tables: readonly string[];',
|
|
431
475
|
' readonly bind: (params: Params) => readonly QueryValue[];',
|
|
432
476
|
' readonly sqlFor?: (params: Params) => string;',
|
package/dist/generate.js
CHANGED
|
@@ -11,6 +11,7 @@ import { emitKotlinModule } from './emit-kotlin.js';
|
|
|
11
11
|
import { emitQueriesModule } from './emit-queries.js';
|
|
12
12
|
import { emitQueriesDartModule } from './emit-queries-dart.js';
|
|
13
13
|
import { emitQueriesKotlinModule } from './emit-queries-kotlin.js';
|
|
14
|
+
import { emitQueriesRustModule } from './emit-queries-rust.js';
|
|
14
15
|
import { emitQueriesSwiftModule } from './emit-queries-swift.js';
|
|
15
16
|
import { emitSwiftModule } from './emit-swift.js';
|
|
16
17
|
import { TypegenError } from './errors.js';
|
|
@@ -347,6 +348,8 @@ export function generate(manifestDir) {
|
|
|
347
348
|
targets.push('kotlin');
|
|
348
349
|
if (manifest.output.dart !== undefined)
|
|
349
350
|
targets.push('dart');
|
|
351
|
+
if (manifest.output.rust !== undefined)
|
|
352
|
+
targets.push('rust');
|
|
350
353
|
const naming = {
|
|
351
354
|
naming: manifest.naming,
|
|
352
355
|
targets,
|
|
@@ -365,7 +368,7 @@ export function generate(manifestDir) {
|
|
|
365
368
|
{ path: modulePath, content: module },
|
|
366
369
|
];
|
|
367
370
|
// Opt-in native emitters — each present only when the manifest requests it.
|
|
368
|
-
const { queryIr: queryIrPath, queries: tsQueriesPath, swift, kotlin, dart, } = manifest.output;
|
|
371
|
+
const { queryIr: queryIrPath, queries: tsQueriesPath, swift, kotlin, dart, rust, } = manifest.output;
|
|
369
372
|
// Named queries: analyzed once (SELECT-only, typed against the IR via
|
|
370
373
|
// SQLite), then emitted per-language into its OWN file so schema-only
|
|
371
374
|
// consumers never churn. Only analyzed when SOME query output is requested.
|
|
@@ -373,7 +376,8 @@ export function generate(manifestDir) {
|
|
|
373
376
|
tsQueriesPath !== undefined ||
|
|
374
377
|
swift?.queriesPath !== undefined ||
|
|
375
378
|
kotlin?.queriesPath !== undefined ||
|
|
376
|
-
dart?.queriesPath !== undefined
|
|
379
|
+
dart?.queriesPath !== undefined ||
|
|
380
|
+
rust?.queriesPath !== undefined;
|
|
377
381
|
const analyzedQueries = wantsQueries
|
|
378
382
|
? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)), naming, resolve(manifestDir, manifest.queries))
|
|
379
383
|
: [];
|
|
@@ -430,6 +434,12 @@ export function generate(manifestDir) {
|
|
|
430
434
|
});
|
|
431
435
|
}
|
|
432
436
|
}
|
|
437
|
+
if (rust !== undefined) {
|
|
438
|
+
outputs.push({
|
|
439
|
+
path: resolve(manifestDir, rust.queriesPath),
|
|
440
|
+
content: emitQueriesRustModule(analyzedQueries, queryHash, ir.irVersion, rust.clientCrate),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
433
443
|
return {
|
|
434
444
|
ir,
|
|
435
445
|
irJson,
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './emit-kotlin.js';
|
|
|
9
9
|
export * from './emit-queries.js';
|
|
10
10
|
export * from './emit-queries-dart.js';
|
|
11
11
|
export * from './emit-queries-kotlin.js';
|
|
12
|
+
export * from './emit-queries-rust.js';
|
|
12
13
|
export * from './emit-queries-swift.js';
|
|
13
14
|
export * from './emit-swift.js';
|
|
14
15
|
export * from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from './emit-kotlin.js';
|
|
|
9
9
|
export * from './emit-queries.js';
|
|
10
10
|
export * from './emit-queries-dart.js';
|
|
11
11
|
export * from './emit-queries-kotlin.js';
|
|
12
|
+
export * from './emit-queries-rust.js';
|
|
12
13
|
export * from './emit-queries-swift.js';
|
|
13
14
|
export * from './emit-swift.js';
|
|
14
15
|
export * from './errors.js';
|
package/dist/lsp.js
CHANGED
|
@@ -186,6 +186,8 @@ export class SyqlLanguageServer {
|
|
|
186
186
|
targets.push('kotlin');
|
|
187
187
|
if (manifest.output.dart !== undefined)
|
|
188
188
|
targets.push('dart');
|
|
189
|
+
if (manifest.output.rust !== undefined)
|
|
190
|
+
targets.push('rust');
|
|
189
191
|
const lookup = {
|
|
190
192
|
kind: 'ready',
|
|
191
193
|
context: {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type NamingMode } from './naming.js';
|
|
2
2
|
/** §7/§8 `.syql` conditional-lowering backend selection. */
|
|
3
3
|
export type ManifestQueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
4
4
|
export declare const MANIFEST_FILENAME = "syncular.json";
|
|
@@ -53,6 +53,13 @@ export interface DartOutput {
|
|
|
53
53
|
/** Opt-in named-queries output path (a sibling `.dart` file). */
|
|
54
54
|
readonly queriesPath?: string;
|
|
55
55
|
}
|
|
56
|
+
/** Rust named-query output. Rust consumes neutral schema IR at runtime, so
|
|
57
|
+
* revision 1 has no separate generated schema source path. */
|
|
58
|
+
export interface RustOutput {
|
|
59
|
+
readonly queriesPath: string;
|
|
60
|
+
/** Rust module identifier for the `syncular-client` Cargo dependency. */
|
|
61
|
+
readonly clientCrate: string;
|
|
62
|
+
}
|
|
56
63
|
export interface ManifestOutput {
|
|
57
64
|
readonly ir: string;
|
|
58
65
|
readonly module: string;
|
|
@@ -66,6 +73,7 @@ export interface ManifestOutput {
|
|
|
66
73
|
readonly swift?: SwiftOutput;
|
|
67
74
|
readonly kotlin?: KotlinOutput;
|
|
68
75
|
readonly dart?: DartOutput;
|
|
76
|
+
readonly rust?: RustOutput;
|
|
69
77
|
}
|
|
70
78
|
export interface Manifest {
|
|
71
79
|
readonly manifestVersion: 1;
|
package/dist/manifest.js
CHANGED
|
@@ -28,19 +28,21 @@
|
|
|
28
28
|
* placeholders (partial templates are unsupported).
|
|
29
29
|
* - `output.ir` / `output.module` are the always-emitted TS defaults;
|
|
30
30
|
* `output.queryIr` opts into the deterministic analyzed-query document.
|
|
31
|
-
* `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native
|
|
31
|
+
* `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native schema
|
|
32
32
|
* emitters — a bare string is the output path; an object carries
|
|
33
33
|
* language-appropriate options (Kotlin `package`/`objectName`, Swift
|
|
34
34
|
* `enumName`; Dart takes `path` only). This is additive within the `output`
|
|
35
35
|
* object (the same forward-extension shape `output.ir`/`output.module`
|
|
36
36
|
* already use); no `manifestVersion` bump — new *recognized* keys, not
|
|
37
37
|
* tolerated-unknown ones. Absent → that language is not generated (TS
|
|
38
|
-
* stays the default).
|
|
38
|
+
* stays the default). `output.rust.queriesPath` opts into Rust named-query
|
|
39
|
+
* source over the neutral schema IR consumed by `syncular-client`.
|
|
39
40
|
* - Unknown keys are hard errors everywhere (fail loud; growth happens by
|
|
40
41
|
* bumping `manifestVersion`), except inside `extensions`, the reserved
|
|
41
42
|
* WP-49 passthrough slot copied verbatim into the IR.
|
|
42
43
|
*/
|
|
43
44
|
import { TypegenError } from './errors.js';
|
|
45
|
+
import { isRustKeyword } from './naming.js';
|
|
44
46
|
export const MANIFEST_FILENAME = 'syncular.json';
|
|
45
47
|
const SOURCE = MANIFEST_FILENAME;
|
|
46
48
|
function fail(message) {
|
|
@@ -137,6 +139,21 @@ function parseDartOutput(value) {
|
|
|
137
139
|
: {}),
|
|
138
140
|
};
|
|
139
141
|
}
|
|
142
|
+
const RUST_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
143
|
+
function parseRustOutput(value) {
|
|
144
|
+
const obj = asObject(value, 'output.rust');
|
|
145
|
+
rejectUnknownKeys(obj, ['queriesPath', 'clientCrate'], 'output.rust');
|
|
146
|
+
const clientCrate = obj.clientCrate === undefined
|
|
147
|
+
? 'syncular_client'
|
|
148
|
+
: asString(obj.clientCrate, 'output.rust.clientCrate');
|
|
149
|
+
if (!RUST_IDENTIFIER_RE.test(clientCrate) || isRustKeyword(clientCrate)) {
|
|
150
|
+
fail(`output.rust.clientCrate must be one Rust identifier, got ${JSON.stringify(clientCrate)}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
queriesPath: asString(obj.queriesPath, 'output.rust.queriesPath'),
|
|
154
|
+
clientCrate,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
140
157
|
function parseTable(value, index) {
|
|
141
158
|
const context = `tables[${index}]`;
|
|
142
159
|
const obj = asObject(value, context);
|
|
@@ -248,9 +265,10 @@ export function parseManifest(raw) {
|
|
|
248
265
|
let swift;
|
|
249
266
|
let kotlin;
|
|
250
267
|
let dart;
|
|
268
|
+
let rust;
|
|
251
269
|
if (obj.output !== undefined) {
|
|
252
270
|
const output = asObject(obj.output, 'output');
|
|
253
|
-
rejectUnknownKeys(output, ['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart'], 'output');
|
|
271
|
+
rejectUnknownKeys(output, ['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart', 'rust'], 'output');
|
|
254
272
|
if (output.ir !== undefined)
|
|
255
273
|
ir = asString(output.ir, 'output.ir');
|
|
256
274
|
if (output.module !== undefined) {
|
|
@@ -268,6 +286,8 @@ export function parseManifest(raw) {
|
|
|
268
286
|
kotlin = parseKotlinOutput(output.kotlin);
|
|
269
287
|
if (output.dart !== undefined)
|
|
270
288
|
dart = parseDartOutput(output.dart);
|
|
289
|
+
if (output.rust !== undefined)
|
|
290
|
+
rust = parseRustOutput(output.rust);
|
|
271
291
|
}
|
|
272
292
|
// Build `output` with only the keys that are set — `exactOptionalPropertyTypes`
|
|
273
293
|
// forbids explicit `undefined` on optional properties.
|
|
@@ -279,6 +299,7 @@ export function parseManifest(raw) {
|
|
|
279
299
|
...(swift !== undefined ? { swift } : {}),
|
|
280
300
|
...(kotlin !== undefined ? { kotlin } : {}),
|
|
281
301
|
...(dart !== undefined ? { dart } : {}),
|
|
302
|
+
...(rust !== undefined ? { rust } : {}),
|
|
282
303
|
};
|
|
283
304
|
if (!Array.isArray(obj.schemaVersions) || obj.schemaVersions.length === 0) {
|
|
284
305
|
fail('schemaVersions must be a non-empty array (§1.5 version history)');
|
package/dist/naming.d.ts
CHANGED
|
@@ -6,7 +6,17 @@ export declare function snakeToCamel(name: string): string;
|
|
|
6
6
|
/** The emitter targets whose keyword sets we police. `ts` is included for
|
|
7
7
|
* completeness (its emitters can quote any property, so its list is the
|
|
8
8
|
* small set that breaks generated FUNCTION/const identifiers). */
|
|
9
|
-
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart';
|
|
9
|
+
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart' | 'rust';
|
|
10
|
+
export declare function isRustKeyword(name: string): boolean;
|
|
11
|
+
/** Pinned Rust identifier conversion from RFC 0006. */
|
|
12
|
+
export declare function rustSnakeCase(name: string): string;
|
|
13
|
+
export declare function rustPascalCase(name: string): string;
|
|
14
|
+
export interface RustNameMapping {
|
|
15
|
+
readonly langName: string;
|
|
16
|
+
readonly rustName: string;
|
|
17
|
+
}
|
|
18
|
+
/** Validate and map QueryIR runtime names to emitted Rust identifiers. */
|
|
19
|
+
export declare function buildRustNamingMap(langNames: readonly string[], context: string, scope: string): RustNameMapping[];
|
|
10
20
|
/** One naming-map entry: the SQL-truth name and its language-facing name. */
|
|
11
21
|
export interface NameMapping {
|
|
12
22
|
readonly sqlName: string;
|
package/dist/naming.js
CHANGED
|
@@ -202,7 +202,130 @@ const TARGET_KEYWORDS = {
|
|
|
202
202
|
'while',
|
|
203
203
|
'with',
|
|
204
204
|
]),
|
|
205
|
+
rust: new Set([
|
|
206
|
+
'abstract',
|
|
207
|
+
'as',
|
|
208
|
+
'async',
|
|
209
|
+
'await',
|
|
210
|
+
'become',
|
|
211
|
+
'box',
|
|
212
|
+
'break',
|
|
213
|
+
'const',
|
|
214
|
+
'continue',
|
|
215
|
+
'crate',
|
|
216
|
+
'do',
|
|
217
|
+
'dyn',
|
|
218
|
+
'else',
|
|
219
|
+
'enum',
|
|
220
|
+
'extern',
|
|
221
|
+
'false',
|
|
222
|
+
'final',
|
|
223
|
+
'fn',
|
|
224
|
+
'for',
|
|
225
|
+
'gen',
|
|
226
|
+
'if',
|
|
227
|
+
'impl',
|
|
228
|
+
'in',
|
|
229
|
+
'let',
|
|
230
|
+
'loop',
|
|
231
|
+
'macro',
|
|
232
|
+
'match',
|
|
233
|
+
'mod',
|
|
234
|
+
'move',
|
|
235
|
+
'mut',
|
|
236
|
+
'override',
|
|
237
|
+
'priv',
|
|
238
|
+
'pub',
|
|
239
|
+
'ref',
|
|
240
|
+
'return',
|
|
241
|
+
'self',
|
|
242
|
+
'static',
|
|
243
|
+
'struct',
|
|
244
|
+
'super',
|
|
245
|
+
'trait',
|
|
246
|
+
'true',
|
|
247
|
+
'try',
|
|
248
|
+
'type',
|
|
249
|
+
'typeof',
|
|
250
|
+
'unsafe',
|
|
251
|
+
'unsized',
|
|
252
|
+
'use',
|
|
253
|
+
'virtual',
|
|
254
|
+
'where',
|
|
255
|
+
'while',
|
|
256
|
+
'yield',
|
|
257
|
+
]),
|
|
205
258
|
};
|
|
259
|
+
export function isRustKeyword(name) {
|
|
260
|
+
return TARGET_KEYWORDS.rust.has(name);
|
|
261
|
+
}
|
|
262
|
+
/** Pinned Rust identifier conversion from RFC 0006. */
|
|
263
|
+
export function rustSnakeCase(name) {
|
|
264
|
+
if (!/^_*[A-Za-z][A-Za-z0-9_]*$/.test(name))
|
|
265
|
+
return name;
|
|
266
|
+
const lead = /^_*/.exec(name)?.[0] ?? '';
|
|
267
|
+
const withoutLead = name.slice(lead.length);
|
|
268
|
+
const trail = /_*$/.exec(withoutLead)?.[0] ?? '';
|
|
269
|
+
const middle = withoutLead.slice(0, withoutLead.length - trail.length);
|
|
270
|
+
const out = [];
|
|
271
|
+
for (let index = 0; index < middle.length; index++) {
|
|
272
|
+
const char = middle[index];
|
|
273
|
+
if (char === '_') {
|
|
274
|
+
if (out.length > 0 && out[out.length - 1] !== '_')
|
|
275
|
+
out.push('_');
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const previous = index > 0 ? middle[index - 1] : undefined;
|
|
279
|
+
const next = index + 1 < middle.length ? middle[index + 1] : undefined;
|
|
280
|
+
const uppercase = char >= 'A' && char <= 'Z';
|
|
281
|
+
const previousLowerOrDigit = previous !== undefined && /[a-z0-9]/.test(previous);
|
|
282
|
+
const acronymBoundary = uppercase &&
|
|
283
|
+
previous !== undefined &&
|
|
284
|
+
/[A-Z]/.test(previous) &&
|
|
285
|
+
next !== undefined &&
|
|
286
|
+
/[a-z]/.test(next);
|
|
287
|
+
if (uppercase &&
|
|
288
|
+
(previousLowerOrDigit || acronymBoundary) &&
|
|
289
|
+
out.length > 0 &&
|
|
290
|
+
out[out.length - 1] !== '_') {
|
|
291
|
+
out.push('_');
|
|
292
|
+
}
|
|
293
|
+
out.push(char.toLowerCase());
|
|
294
|
+
}
|
|
295
|
+
return lead + out.join('') + trail;
|
|
296
|
+
}
|
|
297
|
+
export function rustPascalCase(name) {
|
|
298
|
+
const snake = rustSnakeCase(name);
|
|
299
|
+
const lead = /^_*/.exec(snake)?.[0] ?? '';
|
|
300
|
+
const bare = snake.slice(lead.length).replace(/_*$/, '');
|
|
301
|
+
const suffix = snake.slice(lead.length + bare.length);
|
|
302
|
+
return (lead +
|
|
303
|
+
bare
|
|
304
|
+
.split('_')
|
|
305
|
+
.filter((part) => part.length > 0)
|
|
306
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
307
|
+
.join('') +
|
|
308
|
+
suffix);
|
|
309
|
+
}
|
|
310
|
+
/** Validate and map QueryIR runtime names to emitted Rust identifiers. */
|
|
311
|
+
export function buildRustNamingMap(langNames, context, scope) {
|
|
312
|
+
const seen = new Map();
|
|
313
|
+
return langNames.map((langName) => {
|
|
314
|
+
const rustName = rustSnakeCase(langName);
|
|
315
|
+
if (!/^_*[a-z][a-z0-9_]*$/.test(rustName)) {
|
|
316
|
+
throw new TypegenError(context, `${scope}: ${JSON.stringify(langName)} cannot be emitted as a Rust identifier — alias it in SQL (AS)`);
|
|
317
|
+
}
|
|
318
|
+
if (TARGET_KEYWORDS.rust.has(rustName)) {
|
|
319
|
+
throw new TypegenError(context, `${scope}: ${JSON.stringify(langName)} maps to ${JSON.stringify(rustName)}, a reserved word on the rust target — alias it in SQL (AS)`);
|
|
320
|
+
}
|
|
321
|
+
const clash = seen.get(rustName);
|
|
322
|
+
if (clash !== undefined && clash !== langName) {
|
|
323
|
+
throw new TypegenError(context, `${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(langName)} both map to ${JSON.stringify(rustName)} on the rust target — alias one in SQL (AS)`);
|
|
324
|
+
}
|
|
325
|
+
seen.set(rustName, langName);
|
|
326
|
+
return { langName, rustName };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
206
329
|
/**
|
|
207
330
|
* Map a set of SQL names to language names under `mode`, enforcing the §12
|
|
208
331
|
* hard errors within the scope (one table's columns / one query's projection
|
|
@@ -223,8 +346,9 @@ export function buildNamingMap(sqlNames, mode, context, scope, targets) {
|
|
|
223
346
|
throw new TypegenError(context, `${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(sqlName)} both map to ${JSON.stringify(langName)} under camelCase naming — rename one, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`);
|
|
224
347
|
}
|
|
225
348
|
for (const target of targets) {
|
|
226
|
-
|
|
227
|
-
|
|
349
|
+
const targetName = target === 'rust' ? rustSnakeCase(langName) : langName;
|
|
350
|
+
if (TARGET_KEYWORDS[target].has(targetName)) {
|
|
351
|
+
throw new TypegenError(context, `${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(targetName)}, a reserved word on the ${target} target — rename it, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`);
|
|
228
352
|
}
|
|
229
353
|
if (target === 'dart' && langName.startsWith('_')) {
|
|
230
354
|
throw new TypegenError(context, `${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(langName)} — a leading underscore is library-private on the dart target. Alias it in SQL (AS) or set "naming": "preserve" in syncular.json`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.14",
|
|
4
4
|
"description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"!dist/**/*.test.d.ts"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@syncular/core": "0.15.
|
|
52
|
-
"@syncular/server": "0.15.
|
|
51
|
+
"@syncular/core": "0.15.14",
|
|
52
|
+
"@syncular/server": "0.15.14"
|
|
53
53
|
}
|
|
54
54
|
}
|