@syncular/typegen 0.2.0 → 0.3.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 +25 -10
- package/dist/cli.js +155 -4
- package/dist/emit-dart.js +3 -2
- package/dist/emit-kotlin.js +3 -2
- package/dist/emit-queries-dart.js +87 -15
- package/dist/emit-queries-kotlin.js +89 -15
- package/dist/emit-queries-swift.js +92 -14
- package/dist/emit-queries.js +131 -22
- package/dist/emit-swift.js +3 -2
- package/dist/emit.d.ts +2 -1
- package/dist/emit.js +13 -27
- package/dist/fmt.d.ts +3 -0
- package/dist/fmt.js +356 -0
- package/dist/generate.d.ts +16 -6
- package/dist/generate.js +34 -9
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lower.d.ts +54 -0
- package/dist/lower.js +283 -0
- package/dist/lsp.d.ts +20 -0
- package/dist/lsp.js +376 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.js +20 -0
- package/dist/naming.d.ts +22 -0
- package/dist/naming.js +240 -0
- package/dist/query-ir.d.ts +14 -0
- package/dist/query-ir.js +69 -0
- package/dist/query.d.ts +102 -6
- package/dist/query.js +126 -34
- package/dist/syql.d.ts +83 -0
- package/dist/syql.js +945 -0
- package/package.json +4 -4
- package/src/cli.ts +155 -4
- package/src/emit-dart.ts +3 -2
- package/src/emit-kotlin.ts +3 -2
- package/src/emit-queries-dart.ts +109 -16
- package/src/emit-queries-kotlin.ts +111 -18
- package/src/emit-queries-swift.ts +106 -17
- package/src/emit-queries.ts +179 -28
- package/src/emit-swift.ts +3 -2
- package/src/emit.ts +18 -28
- package/src/fmt.ts +351 -0
- package/src/generate.ts +54 -11
- package/src/index.ts +6 -0
- package/src/lower.ts +331 -0
- package/src/lsp.ts +445 -0
- package/src/manifest.ts +38 -0
- package/src/naming.ts +271 -0
- package/src/query-ir.ts +82 -0
- package/src/query.ts +241 -34
- package/src/syql.ts +1171 -0
package/dist/lsp.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncular lsp` — a minimal `.syql` language server (DESIGN-queries.md
|
|
3
|
+
* §10, staged Q5). Zero dependencies: hand-rolled JSON-RPC over stdio with
|
|
4
|
+
* Content-Length framing, and the SAME parser/checker the generator runs —
|
|
5
|
+
* diagnostics are generate-time truth, not a re-implementation.
|
|
6
|
+
*
|
|
7
|
+
* Capabilities:
|
|
8
|
+
* - diagnostics (on open/change): parse errors, lowering errors (B1,
|
|
9
|
+
* knob conflicts), and — when a `syncular.json` is found above the file —
|
|
10
|
+
* the full SQLite-checked analysis (types, unknown columns, naming
|
|
11
|
+
* collisions).
|
|
12
|
+
* - hover: a query name shows its lowered, checked SQL (what actually
|
|
13
|
+
* runs); an `@fragment` ref shows the fragment body.
|
|
14
|
+
* - definition: an `@fragment` ref jumps to its declaration (same file —
|
|
15
|
+
* fragments are file-scoped).
|
|
16
|
+
*
|
|
17
|
+
* The server core is a pure(ish) class (`SyqlLanguageServer.handle`)
|
|
18
|
+
* so tests drive it without stdio; `runLspStdio` is the thin transport.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
22
|
+
import { TypegenError } from './errors.js';
|
|
23
|
+
import { buildIr, loadMigrations, makeQueryDb } from './generate.js';
|
|
24
|
+
import { MANIFEST_FILENAME, parseManifest } from './manifest.js';
|
|
25
|
+
import { analyzeSyqlFile, parseSyqlFile } from './syql.js';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Text/position helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function offsetToPosition(text, offset) {
|
|
30
|
+
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
31
|
+
let line = 0;
|
|
32
|
+
let lineStart = 0;
|
|
33
|
+
for (let i = 0; i < clamped; i++) {
|
|
34
|
+
if (text[i] === '\n') {
|
|
35
|
+
line += 1;
|
|
36
|
+
lineStart = i + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { line, character: clamped - lineStart };
|
|
40
|
+
}
|
|
41
|
+
function positionToOffset(text, position) {
|
|
42
|
+
let line = 0;
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < text.length && line < position.line) {
|
|
45
|
+
if (text[i] === '\n')
|
|
46
|
+
line += 1;
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return Math.min(text.length, i + position.character);
|
|
50
|
+
}
|
|
51
|
+
/** The `@word` / `word` under the cursor, with its range. */
|
|
52
|
+
function wordAt(text, offset) {
|
|
53
|
+
const isWord = (ch) => ch !== undefined && /[A-Za-z0-9_@]/.test(ch);
|
|
54
|
+
if (!isWord(text[offset]) && !isWord(text[offset - 1]))
|
|
55
|
+
return null;
|
|
56
|
+
let start = isWord(text[offset]) ? offset : offset - 1;
|
|
57
|
+
while (start > 0 && isWord(text[start - 1]))
|
|
58
|
+
start -= 1;
|
|
59
|
+
let end = start;
|
|
60
|
+
while (end < text.length && isWord(text[end]))
|
|
61
|
+
end += 1;
|
|
62
|
+
return { word: text.slice(start, end), start, end };
|
|
63
|
+
}
|
|
64
|
+
function uriToPath(uri) {
|
|
65
|
+
return uri.startsWith('file://') ? decodeURIComponent(uri.slice(7)) : uri;
|
|
66
|
+
}
|
|
67
|
+
function findManifestDir(fromPath) {
|
|
68
|
+
let dir = dirname(fromPath);
|
|
69
|
+
for (let i = 0; i < 20; i++) {
|
|
70
|
+
if (existsSync(resolve(dir, MANIFEST_FILENAME)))
|
|
71
|
+
return dir;
|
|
72
|
+
const parent = dirname(dir);
|
|
73
|
+
if (parent === dir)
|
|
74
|
+
return null;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// The server
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
export class SyqlLanguageServer {
|
|
83
|
+
#documents = new Map();
|
|
84
|
+
#contexts = new Map();
|
|
85
|
+
/** Handle one incoming message; returns the outgoing messages. */
|
|
86
|
+
handle(message) {
|
|
87
|
+
const { method, id } = message;
|
|
88
|
+
if (method === 'initialize') {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
jsonrpc: '2.0',
|
|
92
|
+
id,
|
|
93
|
+
result: {
|
|
94
|
+
capabilities: {
|
|
95
|
+
textDocumentSync: 1, // full
|
|
96
|
+
hoverProvider: true,
|
|
97
|
+
definitionProvider: true,
|
|
98
|
+
},
|
|
99
|
+
serverInfo: { name: 'syncular-syql-lsp', version: '0.0.1' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
if (method === 'textDocument/didOpen') {
|
|
105
|
+
const params = message.params;
|
|
106
|
+
this.#documents.set(params.textDocument.uri, params.textDocument.text);
|
|
107
|
+
return [this.#publishDiagnostics(params.textDocument.uri)];
|
|
108
|
+
}
|
|
109
|
+
if (method === 'textDocument/didChange') {
|
|
110
|
+
const params = message.params;
|
|
111
|
+
const last = params.contentChanges[params.contentChanges.length - 1];
|
|
112
|
+
if (last !== undefined) {
|
|
113
|
+
this.#documents.set(params.textDocument.uri, last.text);
|
|
114
|
+
}
|
|
115
|
+
return [this.#publishDiagnostics(params.textDocument.uri)];
|
|
116
|
+
}
|
|
117
|
+
if (method === 'textDocument/didClose') {
|
|
118
|
+
const params = message.params;
|
|
119
|
+
this.#documents.delete(params.textDocument.uri);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (method === 'textDocument/hover') {
|
|
123
|
+
return [{ jsonrpc: '2.0', id, result: this.#hover(message.params) }];
|
|
124
|
+
}
|
|
125
|
+
if (method === 'textDocument/definition') {
|
|
126
|
+
return [{ jsonrpc: '2.0', id, result: this.#definition(message.params) }];
|
|
127
|
+
}
|
|
128
|
+
if (method === 'shutdown') {
|
|
129
|
+
return [{ jsonrpc: '2.0', id, result: null }];
|
|
130
|
+
}
|
|
131
|
+
if (id !== undefined && method !== undefined) {
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
jsonrpc: '2.0',
|
|
135
|
+
id,
|
|
136
|
+
error: { code: -32601, message: `unhandled method ${method}` },
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
/** Reset cached manifest contexts (e.g. after schema edits). */
|
|
143
|
+
invalidateProjects() {
|
|
144
|
+
this.#contexts = new Map();
|
|
145
|
+
}
|
|
146
|
+
#context(uri) {
|
|
147
|
+
const path = uriToPath(uri);
|
|
148
|
+
const manifestDir = findManifestDir(path);
|
|
149
|
+
if (manifestDir === null)
|
|
150
|
+
return null;
|
|
151
|
+
const cached = this.#contexts.get(manifestDir);
|
|
152
|
+
if (cached !== undefined)
|
|
153
|
+
return cached;
|
|
154
|
+
try {
|
|
155
|
+
const manifest = parseManifest(JSON.parse(readFileSync(resolve(manifestDir, MANIFEST_FILENAME), 'utf8')));
|
|
156
|
+
const migrations = loadMigrations(resolve(manifestDir, manifest.migrations));
|
|
157
|
+
const ir = buildIr(manifest, migrations);
|
|
158
|
+
const { db } = makeQueryDb(ir);
|
|
159
|
+
const targets = ['ts'];
|
|
160
|
+
const context = {
|
|
161
|
+
ir,
|
|
162
|
+
db,
|
|
163
|
+
manifestDir,
|
|
164
|
+
naming: {
|
|
165
|
+
naming: manifest.naming,
|
|
166
|
+
targets,
|
|
167
|
+
backend: manifest.queryBackend,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
this.#contexts.set(manifestDir, context);
|
|
171
|
+
return context;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
this.#contexts.set(manifestDir, null);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
#publishDiagnostics(uri) {
|
|
179
|
+
const text = this.#documents.get(uri) ?? '';
|
|
180
|
+
const diagnostics = this.#diagnose(uri, text);
|
|
181
|
+
return {
|
|
182
|
+
jsonrpc: '2.0',
|
|
183
|
+
method: 'textDocument/publishDiagnostics',
|
|
184
|
+
params: { uri, diagnostics },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
#diagnose(uri, text) {
|
|
188
|
+
const path = uriToPath(uri);
|
|
189
|
+
const context = this.#context(uri);
|
|
190
|
+
try {
|
|
191
|
+
if (context !== null) {
|
|
192
|
+
// The full generate-time analysis: parse + lower + SQLite check.
|
|
193
|
+
const rel = relative(context.manifestDir, path).split('\\').join('/');
|
|
194
|
+
analyzeSyqlFile(rel, text, context.ir, context.db, context.naming);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No manifest reachable: parser + lowering only.
|
|
198
|
+
const parsed = parseSyqlFile(path, text);
|
|
199
|
+
void parsed;
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return [this.#errorToDiagnostic(error, text)];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
#errorToDiagnostic(error, text) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
let range = {
|
|
210
|
+
start: { line: 0, character: 0 },
|
|
211
|
+
end: { line: 0, character: 1 },
|
|
212
|
+
};
|
|
213
|
+
if (error instanceof TypegenError) {
|
|
214
|
+
// Cursor errors carry `file:line`; lowering errors carry
|
|
215
|
+
// `file (query name)` — anchor those at the declaration.
|
|
216
|
+
const lineMatch = /:(\d+): /.exec(message);
|
|
217
|
+
const queryMatch = /\(query ([A-Za-z][A-Za-z0-9]*)\)/.exec(message);
|
|
218
|
+
if (lineMatch !== null) {
|
|
219
|
+
const line = Number.parseInt(lineMatch[1], 10) - 1;
|
|
220
|
+
range = {
|
|
221
|
+
start: { line, character: 0 },
|
|
222
|
+
end: { line, character: 200 },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
else if (queryMatch !== null) {
|
|
226
|
+
try {
|
|
227
|
+
const parsed = parseSyqlFile('doc', text);
|
|
228
|
+
const decl = parsed.queries.find((q) => q.name === queryMatch[1]);
|
|
229
|
+
if (decl !== undefined) {
|
|
230
|
+
const start = offsetToPosition(text, decl.offset);
|
|
231
|
+
range = {
|
|
232
|
+
start,
|
|
233
|
+
end: { line: start.line, character: start.character + 200 },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// fall through to line 0
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { range, severity: 1, source: 'syncular', message };
|
|
243
|
+
}
|
|
244
|
+
#hover(params) {
|
|
245
|
+
const p = params;
|
|
246
|
+
const text = this.#documents.get(p.textDocument.uri);
|
|
247
|
+
if (text === undefined)
|
|
248
|
+
return null;
|
|
249
|
+
const offset = positionToOffset(text, p.position);
|
|
250
|
+
const found = wordAt(text, offset);
|
|
251
|
+
if (found === null)
|
|
252
|
+
return null;
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = parseSyqlFile('doc', text);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
// `@fragment` ref → the fragment's body.
|
|
261
|
+
if (found.word.startsWith('@')) {
|
|
262
|
+
const fragment = parsed.fragments.find((f) => f.name === found.word.slice(1));
|
|
263
|
+
if (fragment === undefined)
|
|
264
|
+
return null;
|
|
265
|
+
return {
|
|
266
|
+
contents: {
|
|
267
|
+
kind: 'markdown',
|
|
268
|
+
value: `\`\`\`sql\n${fragment.body}\n\`\`\``,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// A query name → the lowered, checked SQL (needs the project schema).
|
|
273
|
+
const query = parsed.queries.find((q) => q.name === found.word);
|
|
274
|
+
if (query !== undefined) {
|
|
275
|
+
const context = this.#context(p.textDocument.uri);
|
|
276
|
+
if (context === null)
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
const analyzed = analyzeSyqlFile('doc.syql', text, context.ir, context.db, context.naming).find((q) => q.name === found.word);
|
|
280
|
+
if (analyzed === undefined)
|
|
281
|
+
return null;
|
|
282
|
+
const lines = [
|
|
283
|
+
'```sql',
|
|
284
|
+
analyzed.sql,
|
|
285
|
+
'```',
|
|
286
|
+
'',
|
|
287
|
+
`tables: ${analyzed.tables.join(', ')}`,
|
|
288
|
+
...(analyzed.variants !== undefined
|
|
289
|
+
? [`variants: ${analyzed.variants.length} enumerated statements`]
|
|
290
|
+
: []),
|
|
291
|
+
];
|
|
292
|
+
return { contents: { kind: 'markdown', value: lines.join('\n') } };
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
#definition(params) {
|
|
301
|
+
const p = params;
|
|
302
|
+
const text = this.#documents.get(p.textDocument.uri);
|
|
303
|
+
if (text === undefined)
|
|
304
|
+
return null;
|
|
305
|
+
const offset = positionToOffset(text, p.position);
|
|
306
|
+
const found = wordAt(text, offset);
|
|
307
|
+
if (found === null || !found.word.startsWith('@'))
|
|
308
|
+
return null;
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = parseSyqlFile('doc', text);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const fragment = parsed.fragments.find((f) => f.name === found.word.slice(1));
|
|
317
|
+
if (fragment === undefined)
|
|
318
|
+
return null;
|
|
319
|
+
const start = offsetToPosition(text, fragment.offset);
|
|
320
|
+
return {
|
|
321
|
+
uri: p.textDocument.uri,
|
|
322
|
+
range: {
|
|
323
|
+
start,
|
|
324
|
+
end: {
|
|
325
|
+
line: start.line,
|
|
326
|
+
character: start.character + 'fragment '.length + fragment.name.length,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// stdio transport (Content-Length framing)
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
/** Run the language server over stdio until `exit`. */
|
|
336
|
+
export async function runLspStdio() {
|
|
337
|
+
const server = new SyqlLanguageServer();
|
|
338
|
+
let buffer = Buffer.alloc(0);
|
|
339
|
+
const write = (message) => {
|
|
340
|
+
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
341
|
+
process.stdout.write(Buffer.concat([
|
|
342
|
+
Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii'),
|
|
343
|
+
body,
|
|
344
|
+
]));
|
|
345
|
+
};
|
|
346
|
+
for await (const chunk of process.stdin) {
|
|
347
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
348
|
+
for (;;) {
|
|
349
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
350
|
+
if (headerEnd === -1)
|
|
351
|
+
break;
|
|
352
|
+
const header = buffer.subarray(0, headerEnd).toString('ascii');
|
|
353
|
+
const lengthMatch = /Content-Length: (\d+)/i.exec(header);
|
|
354
|
+
if (lengthMatch === null) {
|
|
355
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const length = Number.parseInt(lengthMatch[1], 10);
|
|
359
|
+
if (buffer.length < headerEnd + 4 + length)
|
|
360
|
+
break;
|
|
361
|
+
const body = buffer.subarray(headerEnd + 4, headerEnd + 4 + length);
|
|
362
|
+
buffer = buffer.subarray(headerEnd + 4 + length);
|
|
363
|
+
let message;
|
|
364
|
+
try {
|
|
365
|
+
message = JSON.parse(body.toString('utf8'));
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (message.method === 'exit')
|
|
371
|
+
return;
|
|
372
|
+
for (const outgoing of server.handle(message))
|
|
373
|
+
write(outgoing);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { NamingMode } from './naming.js';
|
|
2
|
+
/** §7/§8 `.syql` conditional-lowering backend selection. */
|
|
3
|
+
export type ManifestQueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
1
4
|
export declare const MANIFEST_FILENAME = "syncular.json";
|
|
2
5
|
/** Same shorthand the server schema accepts (§3.1). */
|
|
3
6
|
export type ManifestScopeSpec = string | {
|
|
@@ -67,6 +70,14 @@ export interface Manifest {
|
|
|
67
70
|
/** Directory of `.sql` named-query files (default `./queries`). Only read
|
|
68
71
|
* when at least one output requests a named-queries file. */
|
|
69
72
|
readonly queries: string;
|
|
73
|
+
/** §5 casing: "camel" (default) maps snake_case SQL names to camelCase in
|
|
74
|
+
* generated row types/params (queries lower with `AS` aliases); "preserve"
|
|
75
|
+
* keeps SQL-truth names everywhere. */
|
|
76
|
+
readonly naming: NamingMode;
|
|
77
|
+
/** §7 `.syql` backend: "neutralize" (default), "variants" (enumerate
|
|
78
|
+
* whenever eligible), or "auto" (enumerate at ≤ 2 optional groups). The
|
|
79
|
+
* per-query `variants` knob always forces enumeration. */
|
|
80
|
+
readonly queryBackend: ManifestQueryBackend;
|
|
70
81
|
readonly output: ManifestOutput;
|
|
71
82
|
readonly schemaVersions: readonly ManifestSchemaVersion[];
|
|
72
83
|
readonly tables: readonly ManifestTable[];
|
package/dist/manifest.js
CHANGED
|
@@ -209,6 +209,8 @@ export function parseManifest(raw) {
|
|
|
209
209
|
'manifestVersion',
|
|
210
210
|
'migrations',
|
|
211
211
|
'queries',
|
|
212
|
+
'naming',
|
|
213
|
+
'queryBackend',
|
|
212
214
|
'output',
|
|
213
215
|
'schemaVersions',
|
|
214
216
|
'tables',
|
|
@@ -222,6 +224,22 @@ export function parseManifest(raw) {
|
|
|
222
224
|
? './migrations'
|
|
223
225
|
: asString(obj.migrations, 'migrations');
|
|
224
226
|
const queries = obj.queries === undefined ? './queries' : asString(obj.queries, 'queries');
|
|
227
|
+
let naming = 'camel';
|
|
228
|
+
if (obj.naming !== undefined) {
|
|
229
|
+
if (obj.naming !== 'camel' && obj.naming !== 'preserve') {
|
|
230
|
+
fail(`naming must be "camel" or "preserve", got ${JSON.stringify(obj.naming)}`);
|
|
231
|
+
}
|
|
232
|
+
naming = obj.naming;
|
|
233
|
+
}
|
|
234
|
+
let queryBackend = 'neutralize';
|
|
235
|
+
if (obj.queryBackend !== undefined) {
|
|
236
|
+
if (obj.queryBackend !== 'neutralize' &&
|
|
237
|
+
obj.queryBackend !== 'variants' &&
|
|
238
|
+
obj.queryBackend !== 'auto') {
|
|
239
|
+
fail(`queryBackend must be "neutralize", "variants" or "auto", got ${JSON.stringify(obj.queryBackend)}`);
|
|
240
|
+
}
|
|
241
|
+
queryBackend = obj.queryBackend;
|
|
242
|
+
}
|
|
225
243
|
let ir = './syncular.ir.json';
|
|
226
244
|
let module = './syncular.generated.ts';
|
|
227
245
|
let queriesOut;
|
|
@@ -294,6 +312,8 @@ export function parseManifest(raw) {
|
|
|
294
312
|
manifestVersion: 1,
|
|
295
313
|
migrations,
|
|
296
314
|
queries,
|
|
315
|
+
naming,
|
|
316
|
+
queryBackend,
|
|
297
317
|
output: outputSpec,
|
|
298
318
|
schemaVersions,
|
|
299
319
|
tables,
|
package/dist/naming.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Manifest `"naming"`: camelCase emission (default) or SQL-truth names. */
|
|
2
|
+
export type NamingMode = 'camel' | 'preserve';
|
|
3
|
+
/** The pinned §12 snake→camel conversion. Non-identifier names (computed
|
|
4
|
+
* result columns like `count(*)`) are returned unchanged. */
|
|
5
|
+
export declare function snakeToCamel(name: string): string;
|
|
6
|
+
/** The emitter targets whose keyword sets we police. `ts` is included for
|
|
7
|
+
* completeness (its emitters can quote any property, so its list is the
|
|
8
|
+
* small set that breaks generated FUNCTION/const identifiers). */
|
|
9
|
+
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart';
|
|
10
|
+
/** One naming-map entry: the SQL-truth name and its language-facing name. */
|
|
11
|
+
export interface NameMapping {
|
|
12
|
+
readonly sqlName: string;
|
|
13
|
+
readonly langName: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Map a set of SQL names to language names under `mode`, enforcing the §12
|
|
17
|
+
* hard errors within the scope (one table's columns / one query's projection
|
|
18
|
+
* / one query's params). `context` and `scope` name the error location
|
|
19
|
+
* (e.g. `queries/list.sql`, `table todos`); `targets` are the emitters this
|
|
20
|
+
* run generates — keyword hazards are only real on targets that exist.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildNamingMap(sqlNames: readonly string[], mode: NamingMode, context: string, scope: string, targets: readonly NamingTarget[]): NameMapping[];
|