@syncular/typegen 0.2.1 → 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 +2 -2
- 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/emit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { snakeToCamel } from './naming.js';
|
|
1
2
|
const TS_TYPE = {
|
|
2
3
|
string: 'string',
|
|
3
4
|
integer: 'number',
|
|
@@ -82,14 +83,18 @@ function emitSchema(ir) {
|
|
|
82
83
|
lines.push('} as const;');
|
|
83
84
|
return lines.join('\n');
|
|
84
85
|
}
|
|
85
|
-
function emitRowInterfaces(table) {
|
|
86
|
+
function emitRowInterfaces(table, naming) {
|
|
86
87
|
const type = pascalCase(table.name);
|
|
88
|
+
// §5: mutation-input/row keys are the language-facing names; `mutate`
|
|
89
|
+
// normalizes them back through the schema naming map (camel or snake both
|
|
90
|
+
// bind — one map lookup per key).
|
|
91
|
+
const key = (name) => propertyKey(naming === 'camel' ? snakeToCamel(name) : name);
|
|
87
92
|
const lines = [];
|
|
88
93
|
lines.push(`/** One ${table.name} row (§2.4 column order). */`);
|
|
89
94
|
lines.push(`export interface ${type}Row {`);
|
|
90
95
|
for (const column of table.columns) {
|
|
91
96
|
const ts = appTsType(column);
|
|
92
|
-
lines.push(` ${
|
|
97
|
+
lines.push(` ${key(column.name)}: ${ts}${column.nullable ? ' | null' : ''};`);
|
|
93
98
|
}
|
|
94
99
|
lines.push('}');
|
|
95
100
|
lines.push('');
|
|
@@ -98,8 +103,8 @@ function emitRowInterfaces(table) {
|
|
|
98
103
|
for (const column of table.columns) {
|
|
99
104
|
const ts = appTsType(column);
|
|
100
105
|
lines.push(column.nullable
|
|
101
|
-
? ` ${
|
|
102
|
-
: ` ${
|
|
106
|
+
? ` ${key(column.name)}?: ${ts} | null;`
|
|
107
|
+
: ` ${key(column.name)}: ${ts};`);
|
|
103
108
|
}
|
|
104
109
|
lines.push('}');
|
|
105
110
|
lines.push('');
|
|
@@ -108,33 +113,15 @@ function emitRowInterfaces(table) {
|
|
|
108
113
|
for (const column of table.columns) {
|
|
109
114
|
const ts = appTsType(column);
|
|
110
115
|
if (column.name === table.primaryKey) {
|
|
111
|
-
lines.push(` ${
|
|
116
|
+
lines.push(` ${key(column.name)}: ${ts};`);
|
|
112
117
|
}
|
|
113
118
|
else {
|
|
114
|
-
lines.push(` ${
|
|
119
|
+
lines.push(` ${key(column.name)}?: ${ts}${column.nullable ? ' | null' : ''};`);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
lines.push('}');
|
|
118
123
|
return lines.join('\n');
|
|
119
124
|
}
|
|
120
|
-
/**
|
|
121
|
-
* A Kysely-compatible `Database` interface: table name → Row type. This is
|
|
122
|
-
* the generic `@syncular/kysely`'s `SyncularDialect` is parameterized by
|
|
123
|
-
* (`new Kysely<Database>({ dialect })`). Additive — the Row interfaces it
|
|
124
|
-
* references are already emitted above. Property keys use the raw table name
|
|
125
|
-
* (quoted when not an identifier) so `db.selectFrom('table-name')` types.
|
|
126
|
-
*/
|
|
127
|
-
function emitDatabaseInterface(ir) {
|
|
128
|
-
const lines = [];
|
|
129
|
-
lines.push('/** Kysely `Database` interface (table → Row); the generic for');
|
|
130
|
-
lines.push(" * @syncular/kysely's SyncularDialect. */");
|
|
131
|
-
lines.push('export interface Database {');
|
|
132
|
-
for (const table of ir.tables) {
|
|
133
|
-
lines.push(` ${propertyKey(table.name)}: ${pascalCase(table.name)}Row;`);
|
|
134
|
-
}
|
|
135
|
-
lines.push('}');
|
|
136
|
-
return lines.join('\n');
|
|
137
|
-
}
|
|
138
125
|
function emitSubscription(sub) {
|
|
139
126
|
const params = [];
|
|
140
127
|
for (const scope of sub.scopes) {
|
|
@@ -172,7 +159,7 @@ function emitSubscription(sub) {
|
|
|
172
159
|
lines.push('} as const;');
|
|
173
160
|
return lines.join('\n');
|
|
174
161
|
}
|
|
175
|
-
export function emitModule(ir, hash) {
|
|
162
|
+
export function emitModule(ir, hash, naming = 'camel') {
|
|
176
163
|
const parts = [];
|
|
177
164
|
parts.push([
|
|
178
165
|
'// Generated by @syncular/typegen — DO NOT EDIT.',
|
|
@@ -181,9 +168,8 @@ export function emitModule(ir, hash) {
|
|
|
181
168
|
].join('\n'));
|
|
182
169
|
parts.push(emitSchema(ir));
|
|
183
170
|
for (const table of ir.tables) {
|
|
184
|
-
parts.push(emitRowInterfaces(table));
|
|
171
|
+
parts.push(emitRowInterfaces(table, naming));
|
|
185
172
|
}
|
|
186
|
-
parts.push(emitDatabaseInterface(ir));
|
|
187
173
|
for (const sub of ir.subscriptions) {
|
|
188
174
|
parts.push(emitSubscription(sub));
|
|
189
175
|
}
|
package/dist/fmt.d.ts
ADDED
package/dist/fmt.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncular fmt` — the canonical `.syql` formatter (DESIGN-queries.md §10,
|
|
3
|
+
* §12). One style, no options:
|
|
4
|
+
*
|
|
5
|
+
* - SQL keywords lowercase; identifier casing preserved (SQL-truth).
|
|
6
|
+
* - one space after commas; collapsed whitespace elsewhere.
|
|
7
|
+
* - one clause per line in the body; WHERE conjuncts one per line,
|
|
8
|
+
* `and`-prefixed and indented under the `where`.
|
|
9
|
+
* - declarations separated by one blank line; knobs on their own lines.
|
|
10
|
+
* - comments are preserved: a declaration's leading comment block stays
|
|
11
|
+
* above it; comments inside bodies stay on their own lines.
|
|
12
|
+
*
|
|
13
|
+
* The formatter is built on the same parser as the generator (`.syql` that
|
|
14
|
+
* does not parse does not format), and formatting is semantics-preserving
|
|
15
|
+
* by construction — `fmt` output re-parses to the same declarations.
|
|
16
|
+
*/
|
|
17
|
+
import { TypegenError } from './errors.js';
|
|
18
|
+
import { parseSyqlFile } from './syql.js';
|
|
19
|
+
/** SQL keywords the canon lowercases (a pragmatic core set — identifiers
|
|
20
|
+
* that collide with these would need quoting anyway). */
|
|
21
|
+
const SQL_KEYWORDS = new Set(('select from where group by having order limit offset window and or not ' +
|
|
22
|
+
'in is null like glob between exists case when then else end as on using ' +
|
|
23
|
+
'join left right full outer inner cross natural union all except ' +
|
|
24
|
+
'intersect distinct values with recursive asc desc collate cast').split(' '));
|
|
25
|
+
/** Tokenize a body: strings/comments verbatim, words, `:params`,
|
|
26
|
+
* `@fragment` refs, single punctuation chars. */
|
|
27
|
+
function tokenize(body) {
|
|
28
|
+
const tokens = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < body.length) {
|
|
31
|
+
const ch = body[i];
|
|
32
|
+
if (/\s/.test(ch)) {
|
|
33
|
+
i += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (body.startsWith('--', i)) {
|
|
37
|
+
const nl = body.indexOf('\n', i);
|
|
38
|
+
const end = nl === -1 ? body.length : nl;
|
|
39
|
+
tokens.push({ kind: 'comment', text: body.slice(i, end).trimEnd() });
|
|
40
|
+
i = end;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (body.startsWith('/*', i)) {
|
|
44
|
+
const close = body.indexOf('*/', i + 2);
|
|
45
|
+
const end = close === -1 ? body.length : close + 2;
|
|
46
|
+
tokens.push({ kind: 'comment', text: body.slice(i, end) });
|
|
47
|
+
i = end;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === "'") {
|
|
51
|
+
let j = i + 1;
|
|
52
|
+
for (;;) {
|
|
53
|
+
if (j >= body.length)
|
|
54
|
+
break;
|
|
55
|
+
if (body[j] === "'") {
|
|
56
|
+
if (body[j + 1] === "'")
|
|
57
|
+
j += 2;
|
|
58
|
+
else {
|
|
59
|
+
j += 1;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
j += 1;
|
|
65
|
+
}
|
|
66
|
+
tokens.push({ kind: 'string', text: body.slice(i, j) });
|
|
67
|
+
i = j;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === ':' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
|
|
73
|
+
j += 1;
|
|
74
|
+
tokens.push({ kind: 'param', text: body.slice(i, j) });
|
|
75
|
+
i = j;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === '@' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
|
|
81
|
+
j += 1;
|
|
82
|
+
tokens.push({ kind: 'fragment', text: body.slice(i, j) });
|
|
83
|
+
i = j;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
87
|
+
let j = i + 1;
|
|
88
|
+
while (j < body.length && /[A-Za-z0-9_]/.test(body[j]))
|
|
89
|
+
j += 1;
|
|
90
|
+
tokens.push({ kind: 'word', text: body.slice(i, j) });
|
|
91
|
+
i = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Multi-char operators stay one token (`||`, `>=`, `<=`, `<>`, `!=`, `==`).
|
|
95
|
+
const two = body.slice(i, i + 2);
|
|
96
|
+
if (['||', '>=', '<=', '<>', '!=', '=='].includes(two)) {
|
|
97
|
+
tokens.push({ kind: 'punct', text: two });
|
|
98
|
+
i += 2;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
tokens.push({ kind: 'punct', text: ch });
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
/** Render tokens back to one line with canonical spacing + keyword case. */
|
|
107
|
+
function render(tokens) {
|
|
108
|
+
let out = '';
|
|
109
|
+
let prev;
|
|
110
|
+
for (const token of tokens) {
|
|
111
|
+
let text = token.text;
|
|
112
|
+
if (token.kind === 'word' && SQL_KEYWORDS.has(text.toLowerCase())) {
|
|
113
|
+
text = text.toLowerCase();
|
|
114
|
+
}
|
|
115
|
+
const glueLeft = token.kind === 'punct' &&
|
|
116
|
+
(text === ',' || text === ')' || text === ';' || text === '.');
|
|
117
|
+
const glueRight = prev !== undefined &&
|
|
118
|
+
((prev.kind === 'punct' && (prev.text === '(' || prev.text === '.')) ||
|
|
119
|
+
prev.kind === 'fragment');
|
|
120
|
+
if (out.length > 0 && !glueLeft && !glueRight)
|
|
121
|
+
out += ' ';
|
|
122
|
+
out += text;
|
|
123
|
+
prev = token;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** Clause keywords that start a new line at paren depth 0. */
|
|
128
|
+
const CLAUSE_STARTERS = new Set([
|
|
129
|
+
'select',
|
|
130
|
+
'from',
|
|
131
|
+
'where',
|
|
132
|
+
'group',
|
|
133
|
+
'having',
|
|
134
|
+
'order',
|
|
135
|
+
'limit',
|
|
136
|
+
'window',
|
|
137
|
+
'union',
|
|
138
|
+
'except',
|
|
139
|
+
'intersect',
|
|
140
|
+
]);
|
|
141
|
+
/** Format a body: one clause per line; WHERE conjuncts one per line. */
|
|
142
|
+
function formatBody(body) {
|
|
143
|
+
const tokens = tokenize(body);
|
|
144
|
+
const lines = [];
|
|
145
|
+
let current = [];
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let braceDepth = 0;
|
|
148
|
+
let inWhere = false;
|
|
149
|
+
let pendingBetween = 0;
|
|
150
|
+
const flush = () => {
|
|
151
|
+
if (current.length > 0)
|
|
152
|
+
lines.push(render(current));
|
|
153
|
+
current = [];
|
|
154
|
+
};
|
|
155
|
+
for (const token of tokens) {
|
|
156
|
+
if (token.kind === 'comment') {
|
|
157
|
+
flush();
|
|
158
|
+
lines.push(token.text);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (token.kind === 'punct') {
|
|
162
|
+
if (token.text === '(')
|
|
163
|
+
depth += 1;
|
|
164
|
+
else if (token.text === ')')
|
|
165
|
+
depth -= 1;
|
|
166
|
+
else if (token.text === '{')
|
|
167
|
+
braceDepth += 1;
|
|
168
|
+
else if (token.text === '}')
|
|
169
|
+
braceDepth -= 1;
|
|
170
|
+
}
|
|
171
|
+
if (token.kind === 'word' && depth === 0 && braceDepth === 0) {
|
|
172
|
+
const word = token.text.toLowerCase();
|
|
173
|
+
if (CLAUSE_STARTERS.has(word)) {
|
|
174
|
+
flush();
|
|
175
|
+
inWhere = word === 'where';
|
|
176
|
+
pendingBetween = 0;
|
|
177
|
+
}
|
|
178
|
+
else if (inWhere) {
|
|
179
|
+
if (word === 'between')
|
|
180
|
+
pendingBetween += 1;
|
|
181
|
+
else if (word === 'and') {
|
|
182
|
+
if (pendingBetween > 0)
|
|
183
|
+
pendingBetween -= 1;
|
|
184
|
+
else
|
|
185
|
+
flush();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
current.push(token);
|
|
190
|
+
}
|
|
191
|
+
flush();
|
|
192
|
+
// Indent: clauses flush-left; WHERE continuation (`and …`) indented 2.
|
|
193
|
+
return lines.map((line) => /^(and|or)\b/.test(line) || line.startsWith('--') ? ` ${line}` : line);
|
|
194
|
+
}
|
|
195
|
+
function formatSignature(decl) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
const seenGroups = new Set();
|
|
198
|
+
for (const param of decl.params) {
|
|
199
|
+
if (param.group !== undefined) {
|
|
200
|
+
if (seenGroups.has(param.group))
|
|
201
|
+
continue;
|
|
202
|
+
seenGroups.add(param.group);
|
|
203
|
+
const members = decl.params.filter((p) => p.group === param.group);
|
|
204
|
+
parts.push(`${members.map((p) => p.name).join('+')}?`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (param.flag)
|
|
208
|
+
parts.push(`${param.name}?: flag`);
|
|
209
|
+
else
|
|
210
|
+
parts.push(`${param.name}${param.optional ? '?' : ''}`);
|
|
211
|
+
}
|
|
212
|
+
return parts.join(', ');
|
|
213
|
+
}
|
|
214
|
+
/** The leading comment block (verbatim lines) before each declaration. */
|
|
215
|
+
function leadingComments(source) {
|
|
216
|
+
// Map from declaration index (0-based, in order of appearance) to its
|
|
217
|
+
// preceding comment lines.
|
|
218
|
+
const out = new Map();
|
|
219
|
+
let pending = [];
|
|
220
|
+
let declIndex = 0;
|
|
221
|
+
let i = 0;
|
|
222
|
+
while (i < source.length) {
|
|
223
|
+
const ch = source[i];
|
|
224
|
+
if (/\s/.test(ch)) {
|
|
225
|
+
i += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (source.startsWith('--', i)) {
|
|
229
|
+
const nl = source.indexOf('\n', i);
|
|
230
|
+
const end = nl === -1 ? source.length : nl;
|
|
231
|
+
pending.push(source.slice(i, end).trimEnd());
|
|
232
|
+
i = end;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (source.startsWith('/*', i)) {
|
|
236
|
+
const close = source.indexOf('*/', i + 2);
|
|
237
|
+
const end = close === -1 ? source.length : close + 2;
|
|
238
|
+
pending.push(source.slice(i, end));
|
|
239
|
+
i = end;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// A declaration keyword: attach pending comments, then skip through its
|
|
243
|
+
// brace block.
|
|
244
|
+
if (pending.length > 0) {
|
|
245
|
+
out.set(declIndex, pending);
|
|
246
|
+
pending = [];
|
|
247
|
+
}
|
|
248
|
+
declIndex += 1;
|
|
249
|
+
const brace = source.indexOf('{', i);
|
|
250
|
+
if (brace === -1)
|
|
251
|
+
break;
|
|
252
|
+
let depth = 0;
|
|
253
|
+
let j = brace;
|
|
254
|
+
while (j < source.length) {
|
|
255
|
+
const c = source[j];
|
|
256
|
+
if (c === "'") {
|
|
257
|
+
j += 1;
|
|
258
|
+
while (j < source.length && source[j] !== "'")
|
|
259
|
+
j += 1;
|
|
260
|
+
}
|
|
261
|
+
else if (source.startsWith('--', j)) {
|
|
262
|
+
const nl = source.indexOf('\n', j);
|
|
263
|
+
j = nl === -1 ? source.length : nl;
|
|
264
|
+
}
|
|
265
|
+
else if (c === '{')
|
|
266
|
+
depth += 1;
|
|
267
|
+
else if (c === '}') {
|
|
268
|
+
depth -= 1;
|
|
269
|
+
if (depth === 0) {
|
|
270
|
+
j += 1;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
j += 1;
|
|
275
|
+
}
|
|
276
|
+
i = j;
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Format one `.syql` source file into its canonical form. Throws
|
|
281
|
+
* {@link TypegenError} when the source does not parse. */
|
|
282
|
+
export function formatSyql(file, source) {
|
|
283
|
+
const parsed = parseSyqlFile(file, source);
|
|
284
|
+
const comments = leadingComments(source);
|
|
285
|
+
const decls = [];
|
|
286
|
+
// Re-derive declaration order (fragments/queries interleave in-source);
|
|
287
|
+
// parseSyqlFile keeps per-kind order, so re-scan the source for kind
|
|
288
|
+
// keywords to interleave faithfully.
|
|
289
|
+
const orderRe = /\b(query|fragment)\s+([A-Za-z][A-Za-z0-9]*)/g;
|
|
290
|
+
const blanked = source
|
|
291
|
+
.replace(/--[^\n]*/g, (m) => ' '.repeat(m.length))
|
|
292
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
293
|
+
.replace(/'(?:[^']|'')*'/g, (m) => ' '.repeat(m.length));
|
|
294
|
+
const sequence = [];
|
|
295
|
+
let braceDepth = 0;
|
|
296
|
+
let lastIndex = 0;
|
|
297
|
+
for (let i = 0; i < blanked.length; i++) {
|
|
298
|
+
const c = blanked[i];
|
|
299
|
+
if (c === '{')
|
|
300
|
+
braceDepth += 1;
|
|
301
|
+
else if (c === '}')
|
|
302
|
+
braceDepth -= 1;
|
|
303
|
+
else if (braceDepth === 0) {
|
|
304
|
+
orderRe.lastIndex = i;
|
|
305
|
+
const m = orderRe.exec(blanked);
|
|
306
|
+
if (m !== null && m.index === i) {
|
|
307
|
+
sequence.push({ kind: m[1], name: m[2] });
|
|
308
|
+
i = orderRe.lastIndex - 1;
|
|
309
|
+
lastIndex = orderRe.lastIndex;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
void lastIndex;
|
|
314
|
+
sequence.forEach((entry, index) => {
|
|
315
|
+
const lines = [];
|
|
316
|
+
const leading = comments.get(index);
|
|
317
|
+
if (leading !== undefined)
|
|
318
|
+
lines.push(...leading);
|
|
319
|
+
if (entry.kind === 'fragment') {
|
|
320
|
+
const decl = parsed.fragments.find((f) => f.name === entry.name);
|
|
321
|
+
if (decl === undefined)
|
|
322
|
+
return;
|
|
323
|
+
lines.push(`fragment ${decl.name}(${formatSignature(decl)}) {`);
|
|
324
|
+
for (const line of formatBody(decl.body))
|
|
325
|
+
lines.push(` ${line}`);
|
|
326
|
+
lines.push('}');
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const decl = parsed.queries.find((q) => q.name === entry.name);
|
|
330
|
+
if (decl === undefined)
|
|
331
|
+
return;
|
|
332
|
+
const knobs = [];
|
|
333
|
+
if (decl.orderBy !== undefined) {
|
|
334
|
+
const dir = decl.orderBy.defaultDir === 'desc' ? ' desc' : '';
|
|
335
|
+
knobs.push(` orderBy ${decl.orderBy.allowed.join(' | ')} default ${decl.orderBy.defaultColumn}${dir}`);
|
|
336
|
+
}
|
|
337
|
+
if (decl.limit !== undefined) {
|
|
338
|
+
const parts = [];
|
|
339
|
+
if (decl.limit.max !== undefined)
|
|
340
|
+
parts.push(`max ${decl.limit.max}`);
|
|
341
|
+
if (decl.limit.default !== undefined) {
|
|
342
|
+
parts.push(`default ${decl.limit.default}`);
|
|
343
|
+
}
|
|
344
|
+
knobs.push(` limit ${parts.join(' ')}`);
|
|
345
|
+
}
|
|
346
|
+
if (decl.variants === true)
|
|
347
|
+
knobs.push(' variants');
|
|
348
|
+
lines.push(`query ${decl.name}(${formatSignature(decl)})${knobs.length > 0 ? `\n${knobs.join('\n')}\n{` : ' {'}`);
|
|
349
|
+
for (const line of formatBody(decl.body))
|
|
350
|
+
lines.push(` ${line}`);
|
|
351
|
+
lines.push('}');
|
|
352
|
+
}
|
|
353
|
+
decls.push({ order: index, text: lines.join('\n') });
|
|
354
|
+
});
|
|
355
|
+
return `${decls.map((d) => d.text).join('\n\n')}\n`;
|
|
356
|
+
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type IrDocument } from './ir.js';
|
|
6
6
|
import { type Manifest } from './manifest.js';
|
|
7
|
-
import { type AnalyzedQuery } from './query.js';
|
|
7
|
+
import { type AnalyzedQuery, type QueryDb, type QueryNamingOptions } from './query.js';
|
|
8
8
|
export interface MigrationInput {
|
|
9
9
|
/** Directory name, e.g. `0001_initial`. */
|
|
10
10
|
readonly name: string;
|
|
@@ -15,19 +15,29 @@ export interface MigrationInput {
|
|
|
15
15
|
export declare function buildIr(manifest: Manifest, migrations: readonly MigrationInput[]): IrDocument;
|
|
16
16
|
/** Load `NNNN_name/up.sql` migrations, ordered by numeric prefix. */
|
|
17
17
|
export declare function loadMigrations(migrationsDir: string): MigrationInput[];
|
|
18
|
-
/** One
|
|
19
|
-
*
|
|
18
|
+
/** One named-query file (`.sql` — the compatibility floor — or `.syql`, the
|
|
19
|
+
* DSL): path relative to the queries root (forward slashes) + raw contents.
|
|
20
|
+
* A file may hold multiple statements/declarations. */
|
|
20
21
|
export interface QueryInput {
|
|
21
22
|
/** Path relative to the queries root, forward-slashed (e.g.
|
|
22
|
-
* `billing/invoices/list.sql`). Drives the path-derived default name
|
|
23
|
+
* `billing/invoices/list.sql`). Drives the path-derived default name for
|
|
24
|
+
* `.sql`; `.syql` queries carry their own names. */
|
|
23
25
|
readonly file: string;
|
|
24
26
|
readonly sql: string;
|
|
25
27
|
}
|
|
26
|
-
/** Load `queries/**\/*.sql` recursively, ordered by relative path
|
|
28
|
+
/** Load `queries/**\/*.{sql,syql}` recursively, ordered by relative path
|
|
27
29
|
* (deterministic emission). Folders are pure organization. */
|
|
28
30
|
export declare function loadQueries(queriesDir: string): QueryInput[];
|
|
31
|
+
/** A {@link QueryDb} backed by an in-memory bun:sqlite built from the IR DDL.
|
|
32
|
+
* `analyze` prepares the statement (validating references) and reads its
|
|
33
|
+
* column names + declared types; it executes once against the empty DB so
|
|
34
|
+
* `declaredTypes` (decltype) is populated. */
|
|
35
|
+
export declare function makeQueryDb(ir: IrDocument): {
|
|
36
|
+
db: QueryDb;
|
|
37
|
+
close: () => void;
|
|
38
|
+
};
|
|
29
39
|
/** Analyze every query file against the IR (SELECT-only, typed, deps). */
|
|
30
|
-
export declare function analyzeQueries(ir: IrDocument, queries: readonly QueryInput[]): AnalyzedQuery[];
|
|
40
|
+
export declare function analyzeQueries(ir: IrDocument, queries: readonly QueryInput[], naming?: QueryNamingOptions): AnalyzedQuery[];
|
|
31
41
|
/** One generated artifact: an absolute output path and its exact bytes. */
|
|
32
42
|
export interface GeneratedOutput {
|
|
33
43
|
readonly path: string;
|
package/dist/generate.js
CHANGED
|
@@ -16,8 +16,10 @@ import { emitSwiftModule } from './emit-swift.js';
|
|
|
16
16
|
import { TypegenError } from './errors.js';
|
|
17
17
|
import { canonicalizeExtensions, IR_VERSION, irHash, serializeIr, } from './ir.js';
|
|
18
18
|
import { MANIFEST_FILENAME, parseManifest, } from './manifest.js';
|
|
19
|
+
import { buildNamingMap } from './naming.js';
|
|
19
20
|
import { analyzeQueryFile, synthesizeDdl, } from './query.js';
|
|
20
21
|
import { applyMigrationSql } from './sql.js';
|
|
22
|
+
import { analyzeSyqlFile } from './syql.js';
|
|
21
23
|
/** Same grammar the server compiles (§3.1): `prefix:{variable}`. */
|
|
22
24
|
const PATTERN_RE = /^([^{}]+):\{([^{}:]+)\}$/;
|
|
23
25
|
/** Whole-value placeholder: `{paramName}`. */
|
|
@@ -208,8 +210,8 @@ export function loadMigrations(migrationsDir) {
|
|
|
208
210
|
return { name, sql: readFileSync(upPath, 'utf8') };
|
|
209
211
|
});
|
|
210
212
|
}
|
|
211
|
-
/** Recursively collect `*.sql` paths under `dir`, relative to
|
|
212
|
-
* (forward-slashed), sorted for deterministic emission. */
|
|
213
|
+
/** Recursively collect `*.sql` + `*.syql` paths under `dir`, relative to
|
|
214
|
+
* `root` (forward-slashed), sorted for deterministic emission. */
|
|
213
215
|
function collectSqlFiles(root, dir) {
|
|
214
216
|
const out = [];
|
|
215
217
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -217,13 +219,14 @@ function collectSqlFiles(root, dir) {
|
|
|
217
219
|
if (entry.isDirectory()) {
|
|
218
220
|
out.push(...collectSqlFiles(root, abs));
|
|
219
221
|
}
|
|
220
|
-
else if (entry.isFile() &&
|
|
222
|
+
else if (entry.isFile() &&
|
|
223
|
+
(entry.name.endsWith('.sql') || entry.name.endsWith('.syql'))) {
|
|
221
224
|
out.push(relative(root, abs).split(sep).join('/'));
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
return out;
|
|
225
228
|
}
|
|
226
|
-
/** Load `queries/**\/*.sql` recursively, ordered by relative path
|
|
229
|
+
/** Load `queries/**\/*.{sql,syql}` recursively, ordered by relative path
|
|
227
230
|
* (deterministic emission). Folders are pure organization. */
|
|
228
231
|
export function loadQueries(queriesDir) {
|
|
229
232
|
if (!existsSync(queriesDir))
|
|
@@ -239,7 +242,7 @@ export function loadQueries(queriesDir) {
|
|
|
239
242
|
* `analyze` prepares the statement (validating references) and reads its
|
|
240
243
|
* column names + declared types; it executes once against the empty DB so
|
|
241
244
|
* `declaredTypes` (decltype) is populated. */
|
|
242
|
-
function makeQueryDb(ir) {
|
|
245
|
+
export function makeQueryDb(ir) {
|
|
243
246
|
const sqlite = new Database(':memory:');
|
|
244
247
|
sqlite.run(synthesizeDdl(ir));
|
|
245
248
|
const db = {
|
|
@@ -262,13 +265,17 @@ function makeQueryDb(ir) {
|
|
|
262
265
|
return { db, close: () => sqlite.close() };
|
|
263
266
|
}
|
|
264
267
|
/** Analyze every query file against the IR (SELECT-only, typed, deps). */
|
|
265
|
-
export function analyzeQueries(ir, queries) {
|
|
268
|
+
export function analyzeQueries(ir, queries, naming) {
|
|
266
269
|
if (queries.length === 0)
|
|
267
270
|
return [];
|
|
268
271
|
const { db, close } = makeQueryDb(ir);
|
|
269
272
|
try {
|
|
270
273
|
// Each file may hold multiple statements; flatten in path order (stable).
|
|
271
|
-
|
|
274
|
+
// Frontends dispatch on extension; both produce the same AnalyzedQuery
|
|
275
|
+
// shape (§1 — nothing downstream knows the frontend).
|
|
276
|
+
const analyzed = queries.flatMap((q) => q.file.endsWith('.syql')
|
|
277
|
+
? analyzeSyqlFile(q.file, q.sql, ir, db, naming)
|
|
278
|
+
: analyzeQueryFile(q.file, q.sql, ir, db, naming));
|
|
272
279
|
// Names must be unique across the whole manifest — the filesystem no longer
|
|
273
280
|
// guarantees uniqueness once `-- name:` overrides exist. Report BOTH source
|
|
274
281
|
// locations (file + statement position) on a collision.
|
|
@@ -302,9 +309,27 @@ export function generate(manifestDir) {
|
|
|
302
309
|
const manifest = parseManifest(raw);
|
|
303
310
|
const migrations = loadMigrations(resolve(manifestDir, manifest.migrations));
|
|
304
311
|
const ir = buildIr(manifest, migrations);
|
|
312
|
+
// §5/§12 naming: the emitter targets this run generates (keyword hazards
|
|
313
|
+
// are only real on targets that exist), and the per-table collision check
|
|
314
|
+
// for the generated row types.
|
|
315
|
+
const targets = ['ts'];
|
|
316
|
+
if (manifest.output.swift !== undefined)
|
|
317
|
+
targets.push('swift');
|
|
318
|
+
if (manifest.output.kotlin !== undefined)
|
|
319
|
+
targets.push('kotlin');
|
|
320
|
+
if (manifest.output.dart !== undefined)
|
|
321
|
+
targets.push('dart');
|
|
322
|
+
const naming = {
|
|
323
|
+
naming: manifest.naming,
|
|
324
|
+
targets,
|
|
325
|
+
backend: manifest.queryBackend,
|
|
326
|
+
};
|
|
327
|
+
for (const table of ir.tables) {
|
|
328
|
+
buildNamingMap(table.columns.map((c) => c.name), manifest.naming, MANIFEST_FILENAME, `table ${table.name}`, targets);
|
|
329
|
+
}
|
|
305
330
|
const irJson = serializeIr(ir);
|
|
306
331
|
const hash = irHash(irJson);
|
|
307
|
-
const module = emitModule(ir, hash);
|
|
332
|
+
const module = emitModule(ir, hash, manifest.naming);
|
|
308
333
|
const irPath = resolve(manifestDir, manifest.output.ir);
|
|
309
334
|
const modulePath = resolve(manifestDir, manifest.output.module);
|
|
310
335
|
const outputs = [
|
|
@@ -321,7 +346,7 @@ export function generate(manifestDir) {
|
|
|
321
346
|
kotlin?.queriesPath !== undefined ||
|
|
322
347
|
dart?.queriesPath !== undefined;
|
|
323
348
|
const analyzedQueries = wantsQueries
|
|
324
|
-
? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)))
|
|
349
|
+
? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)), naming)
|
|
325
350
|
: [];
|
|
326
351
|
if (wantsQueries && analyzedQueries.length === 0) {
|
|
327
352
|
throw new TypegenError(resolve(manifestDir, manifest.queries), `an output requests named queries but no .sql files were found in ${manifest.queries} — add a query file or drop the queries output`);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,14 @@ export * from './emit-queries-kotlin.js';
|
|
|
12
12
|
export * from './emit-queries-swift.js';
|
|
13
13
|
export * from './emit-swift.js';
|
|
14
14
|
export * from './errors.js';
|
|
15
|
+
export * from './fmt.js';
|
|
15
16
|
export * from './generate.js';
|
|
16
17
|
export * from './ir.js';
|
|
18
|
+
export * from './lower.js';
|
|
19
|
+
export * from './lsp.js';
|
|
17
20
|
export * from './manifest.js';
|
|
21
|
+
export * from './naming.js';
|
|
18
22
|
export * from './query.js';
|
|
23
|
+
export * from './query-ir.js';
|
|
19
24
|
export * from './sql.js';
|
|
25
|
+
export * from './syql.js';
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,14 @@ export * from './emit-queries-kotlin.js';
|
|
|
12
12
|
export * from './emit-queries-swift.js';
|
|
13
13
|
export * from './emit-swift.js';
|
|
14
14
|
export * from './errors.js';
|
|
15
|
+
export * from './fmt.js';
|
|
15
16
|
export * from './generate.js';
|
|
16
17
|
export * from './ir.js';
|
|
18
|
+
export * from './lower.js';
|
|
19
|
+
export * from './lsp.js';
|
|
17
20
|
export * from './manifest.js';
|
|
21
|
+
export * from './naming.js';
|
|
18
22
|
export * from './query.js';
|
|
23
|
+
export * from './query-ir.js';
|
|
19
24
|
export * from './sql.js';
|
|
25
|
+
export * from './syql.js';
|