@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.
Files changed (51) hide show
  1. package/README.md +25 -10
  2. package/dist/cli.js +155 -4
  3. package/dist/emit-dart.js +3 -2
  4. package/dist/emit-kotlin.js +3 -2
  5. package/dist/emit-queries-dart.js +87 -15
  6. package/dist/emit-queries-kotlin.js +89 -15
  7. package/dist/emit-queries-swift.js +92 -14
  8. package/dist/emit-queries.js +131 -22
  9. package/dist/emit-swift.js +3 -2
  10. package/dist/emit.d.ts +2 -1
  11. package/dist/emit.js +13 -27
  12. package/dist/fmt.d.ts +3 -0
  13. package/dist/fmt.js +356 -0
  14. package/dist/generate.d.ts +16 -6
  15. package/dist/generate.js +34 -9
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +6 -0
  18. package/dist/lower.d.ts +54 -0
  19. package/dist/lower.js +283 -0
  20. package/dist/lsp.d.ts +20 -0
  21. package/dist/lsp.js +376 -0
  22. package/dist/manifest.d.ts +11 -0
  23. package/dist/manifest.js +20 -0
  24. package/dist/naming.d.ts +22 -0
  25. package/dist/naming.js +240 -0
  26. package/dist/query-ir.d.ts +14 -0
  27. package/dist/query-ir.js +69 -0
  28. package/dist/query.d.ts +102 -6
  29. package/dist/query.js +126 -34
  30. package/dist/syql.d.ts +83 -0
  31. package/dist/syql.js +945 -0
  32. package/package.json +4 -4
  33. package/src/cli.ts +155 -4
  34. package/src/emit-dart.ts +3 -2
  35. package/src/emit-kotlin.ts +3 -2
  36. package/src/emit-queries-dart.ts +109 -16
  37. package/src/emit-queries-kotlin.ts +111 -18
  38. package/src/emit-queries-swift.ts +106 -17
  39. package/src/emit-queries.ts +179 -28
  40. package/src/emit-swift.ts +3 -2
  41. package/src/emit.ts +18 -28
  42. package/src/fmt.ts +351 -0
  43. package/src/generate.ts +54 -11
  44. package/src/index.ts +6 -0
  45. package/src/lower.ts +331 -0
  46. package/src/lsp.ts +445 -0
  47. package/src/manifest.ts +38 -0
  48. package/src/naming.ts +271 -0
  49. package/src/query-ir.ts +82 -0
  50. package/src/query.ts +241 -34
  51. package/src/syql.ts +1171 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/typegen",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -31,7 +31,7 @@
31
31
  "exports": {
32
32
  ".": {
33
33
  "bun": "./src/index.ts",
34
- "browser": "./src/index.ts",
34
+ "browser": "./dist/index.js",
35
35
  "import": {
36
36
  "types": "./dist/index.d.ts",
37
37
  "default": "./dist/index.js"
@@ -48,7 +48,7 @@
48
48
  "!dist/**/*.test.d.ts"
49
49
  ],
50
50
  "devDependencies": {
51
- "@syncular/core": "0.2.0",
52
- "@syncular/server": "0.2.0"
51
+ "@syncular/core": "0.2.1",
52
+ "@syncular/server": "0.2.1"
53
53
  }
54
54
  }
package/src/cli.ts CHANGED
@@ -15,11 +15,13 @@
15
15
  * The generate CONTRACT (inputs, outputs, byte-exact `--check`) is unchanged;
16
16
  * this file only adds `init`, `--watch`, and friendlier errors.
17
17
  */
18
- import { watch } from 'node:fs';
18
+ import { existsSync, readFileSync, watch, writeFileSync } from 'node:fs';
19
19
  import { resolve } from 'node:path';
20
- import { checkOutputs, generate, writeOutputs } from './generate';
20
+ import { formatSyql } from './fmt';
21
+ import { checkOutputs, generate, loadQueries, writeOutputs } from './generate';
21
22
  import { InitError, initProject } from './init';
22
- import { MANIFEST_FILENAME } from './manifest';
23
+ import { runLspStdio } from './lsp';
24
+ import { MANIFEST_FILENAME, parseManifest } from './manifest';
23
25
 
24
26
  const DOCS_HINT =
25
27
  'See the schema guide: https://github.com/bkniffler/syncular (guide-schema), ' +
@@ -29,12 +31,22 @@ const USAGE = `usage: syncular <command> [options]
29
31
 
30
32
  commands:
31
33
  generate build the IR + typed module from syncular.json + migrations/
34
+ fmt format .syql query files canonically (one style, no options)
35
+ lsp run the .syql language server over stdio (editor tooling)
32
36
  init scaffold a starter syncular.json + migrations/0001_initial
33
37
 
34
38
  generate options:
35
39
  --manifest-dir <dir> directory holding syncular.json (default: .)
36
40
  --check fail unless generated files are byte-exactly fresh
37
41
  --watch regenerate on file change (Ctrl-C to stop)
42
+ --print <name> print one named query's lowered, checked SQL
43
+ (params, tables, knob variants) and exit
44
+
45
+ fmt options:
46
+ [files…] .syql files to format (default: the manifest's
47
+ queries directory, recursively)
48
+ --manifest-dir <dir> directory holding syncular.json (default: .)
49
+ --check fail unless every file is already canonical
38
50
 
39
51
  init options:
40
52
  --manifest-dir <dir> directory to scaffold into (default: .)`;
@@ -48,10 +60,16 @@ interface GenerateArgs {
48
60
  manifestDir: string;
49
61
  check: boolean;
50
62
  watch: boolean;
63
+ print: string | undefined;
51
64
  }
52
65
 
53
66
  function parseGenerateArgs(argv: readonly string[]): GenerateArgs {
54
- const args: GenerateArgs = { manifestDir: '.', check: false, watch: false };
67
+ const args: GenerateArgs = {
68
+ manifestDir: '.',
69
+ check: false,
70
+ watch: false,
71
+ print: undefined,
72
+ };
55
73
  for (let i = 0; i < argv.length; i++) {
56
74
  const arg = argv[i];
57
75
  if (arg === '--check') {
@@ -63,6 +81,11 @@ function parseGenerateArgs(argv: readonly string[]): GenerateArgs {
63
81
  if (value === undefined) fail('--manifest-dir requires a value');
64
82
  args.manifestDir = value;
65
83
  i += 1;
84
+ } else if (arg === '--print') {
85
+ const value = argv[i + 1];
86
+ if (value === undefined) fail('--print requires a query name');
87
+ args.print = value;
88
+ i += 1;
66
89
  } else {
67
90
  fail(`unknown argument ${JSON.stringify(arg)}\n${USAGE}`);
68
91
  }
@@ -70,9 +93,59 @@ function parseGenerateArgs(argv: readonly string[]): GenerateArgs {
70
93
  if (args.check && args.watch) {
71
94
  fail('--check and --watch cannot be combined');
72
95
  }
96
+ if (args.print !== undefined && (args.check || args.watch)) {
97
+ fail('--print cannot be combined with --check/--watch');
98
+ }
73
99
  return args;
74
100
  }
75
101
 
102
+ /** `generate --print <name>`: "what does this actually run" — the lowered,
103
+ * checked SQL for one named query, plus its params/tables/knob variants. */
104
+ function runGeneratePrint(args: GenerateArgs, name: string): void {
105
+ let result: ReturnType<typeof generate>;
106
+ try {
107
+ result = generate(args.manifestDir);
108
+ } catch (error) {
109
+ fail(friendlyGenerateError(error));
110
+ }
111
+ const query = result.queries.find((q) => q.name === name);
112
+ if (query === undefined) {
113
+ const known = result.queries.map((q) => q.name).join(', ');
114
+ fail(
115
+ `no named query ${JSON.stringify(name)} — this manifest defines: ${known.length > 0 ? known : '(none)'}`,
116
+ );
117
+ }
118
+ console.log(`-- ${query.name} (${query.file})`);
119
+ console.log(`-- tables: ${query.tables.join(', ')}`);
120
+ if (query.params.length > 0) {
121
+ console.log('-- params:');
122
+ for (const param of query.params) {
123
+ const marks = [
124
+ param.optional === true ? 'optional' : 'required',
125
+ ...(param.flag === true ? ['flag'] : []),
126
+ ...(param.group !== undefined ? [`group ${param.group}`] : []),
127
+ ].join(', ');
128
+ console.log(`-- :${param.name} ${param.type} (${marks})`);
129
+ }
130
+ }
131
+ console.log(query.sql);
132
+ if (query.orderBy !== undefined) {
133
+ console.log('-- orderBy variants (each checked against the schema):');
134
+ const base = query.positionalSqlBase ?? '';
135
+ for (const col of query.orderBy.allowed) {
136
+ const isDefault = col.name === query.orderBy.defaultColumn;
137
+ console.log(
138
+ `-- ${col.langName}: ${base} order by ${col.name} asc|desc${query.positionalLimitTail ?? ''}${isDefault ? ' (default)' : ''}`,
139
+ );
140
+ }
141
+ }
142
+ if (query.limit !== undefined) {
143
+ console.log(
144
+ `-- limit: bound value, default ${query.limit.default ?? query.limit.max}${query.limit.max !== undefined ? `, clamped to ${query.limit.max}` : ''}`,
145
+ );
146
+ }
147
+ }
148
+
76
149
  function parseManifestDir(argv: readonly string[]): string {
77
150
  let manifestDir = '.';
78
151
  for (let i = 0; i < argv.length; i++) {
@@ -150,10 +223,80 @@ function runGenerateWatch(args: GenerateArgs): void {
150
223
  });
151
224
  }
152
225
 
226
+ /** `syncular fmt`: canonicalize `.syql` files in place (or `--check`). */
227
+ function runFmt(argv: readonly string[]): void {
228
+ let manifestDir = '.';
229
+ let check = false;
230
+ const files: string[] = [];
231
+ for (let i = 0; i < argv.length; i++) {
232
+ const arg = argv[i] as string;
233
+ if (arg === '--check') check = true;
234
+ else if (arg === '--manifest-dir') {
235
+ const value = argv[i + 1];
236
+ if (value === undefined) fail('--manifest-dir requires a value');
237
+ manifestDir = value;
238
+ i += 1;
239
+ } else if (arg.startsWith('--')) {
240
+ fail(`unknown argument ${JSON.stringify(arg)}\n${USAGE}`);
241
+ } else {
242
+ files.push(arg);
243
+ }
244
+ }
245
+ let targets: string[];
246
+ if (files.length > 0) {
247
+ targets = files.map((f) => resolve(f));
248
+ } else {
249
+ const manifestPath = resolve(manifestDir, MANIFEST_FILENAME);
250
+ if (!existsSync(manifestPath)) {
251
+ fail(
252
+ `no files given and no ${MANIFEST_FILENAME} in ${manifestDir} — pass .syql files or --manifest-dir`,
253
+ );
254
+ }
255
+ const manifest = parseManifest(
256
+ JSON.parse(readFileSync(manifestPath, 'utf8')),
257
+ );
258
+ const queriesDir = resolve(manifestDir, manifest.queries);
259
+ targets = loadQueries(queriesDir)
260
+ .filter((q) => q.file.endsWith('.syql'))
261
+ .map((q) => resolve(queriesDir, q.file));
262
+ }
263
+ if (targets.length === 0) {
264
+ console.log('no .syql files to format');
265
+ return;
266
+ }
267
+ let drifted = 0;
268
+ for (const path of targets) {
269
+ if (!path.endsWith('.syql')) {
270
+ fail(`fmt formats .syql files only, got ${path}`);
271
+ }
272
+ const source = readFileSync(path, 'utf8');
273
+ let formatted: string;
274
+ try {
275
+ formatted = formatSyql(path, source);
276
+ } catch (error) {
277
+ fail(error instanceof Error ? error.message : String(error));
278
+ }
279
+ if (formatted === source) continue;
280
+ drifted += 1;
281
+ if (check) {
282
+ console.error(`${path}: not canonical — run \`syncular fmt\``);
283
+ } else {
284
+ writeFileSync(path, formatted, 'utf8');
285
+ console.log(`formatted ${path}`);
286
+ }
287
+ }
288
+ if (check && drifted > 0) process.exit(1);
289
+ if (drifted === 0) console.log('all .syql files canonical');
290
+ }
291
+
153
292
  export function runCli(argv: readonly string[]): void {
154
293
  const command = argv[0];
155
294
  if (command === 'generate') {
156
295
  const args = parseGenerateArgs(argv.slice(1));
296
+ if (args.print !== undefined) {
297
+ runGeneratePrint(args, args.print);
298
+ return;
299
+ }
157
300
  if (args.watch) {
158
301
  runGenerateWatch(args);
159
302
  return;
@@ -161,6 +304,14 @@ export function runCli(argv: readonly string[]): void {
161
304
  runGenerateOnce(args);
162
305
  return;
163
306
  }
307
+ if (command === 'fmt') {
308
+ runFmt(argv.slice(1));
309
+ return;
310
+ }
311
+ if (command === 'lsp') {
312
+ void runLspStdio();
313
+ return;
314
+ }
164
315
  if (command === 'init') {
165
316
  const dir = parseManifestDir(argv.slice(1));
166
317
  try {
package/src/emit-dart.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * verifies freshness byte-exactly.
15
15
  */
16
16
  import type { IrColumnType, IrDocument, IrSubscription, IrTable } from './ir';
17
+ import { snakeToCamel } from './naming';
17
18
 
18
19
  /** §2.4 column type → honest Dart type. `json`/`blob_ref` are the raw
19
20
  * canonical JSON string; `bytes`/`crdt` are opaque `List<int>`. */
@@ -45,9 +46,9 @@ function pascalCase(name: string): string {
45
46
  .join('');
46
47
  }
47
48
 
49
+ /** Language-facing field name — the pinned §12 naming map. */
48
50
  function camelCase(name: string): string {
49
- const pascal = pascalCase(name);
50
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
51
+ return snakeToCamel(name);
51
52
  }
52
53
 
53
54
  function quote(value: string): string {
@@ -13,6 +13,7 @@
13
13
  * the IR hash so `--check` verifies freshness byte-exactly.
14
14
  */
15
15
  import type { IrColumnType, IrDocument, IrSubscription, IrTable } from './ir';
16
+ import { snakeToCamel } from './naming';
16
17
 
17
18
  /** §2.4 column type → honest Kotlin type. `json`/`blob_ref` are the raw
18
19
  * canonical JSON string; `bytes`/`crdt` are opaque `ByteArray`. */
@@ -44,9 +45,9 @@ function pascalCase(name: string): string {
44
45
  .join('');
45
46
  }
46
47
 
48
+ /** Language-facing field name — the pinned §12 naming map. */
47
49
  function camelCase(name: string): string {
48
- const pascal = pascalCase(name);
49
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
50
+ return snakeToCamel(name);
50
51
  }
51
52
 
52
53
  function quote(value: string): string {
@@ -8,7 +8,8 @@
8
8
  * Header carries the IR hash for byte-exact `--check`.
9
9
  */
10
10
  import type { IrColumnType } from './ir';
11
- import type { AnalyzedQuery, QueryColumn } from './query';
11
+ import { snakeToCamel } from './naming';
12
+ import type { AnalyzedQuery, QueryColumn, QueryParam } from './query';
12
13
 
13
14
  const DART_TYPE: Readonly<Record<IrColumnType, string>> = {
14
15
  string: 'String',
@@ -29,9 +30,9 @@ function pascalCase(name: string): string {
29
30
  .join('');
30
31
  }
31
32
 
33
+ /** Language-facing field name — the pinned §12 naming map. */
32
34
  function camelCase(name: string): string {
33
- const pascal = pascalCase(name);
34
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
35
+ return snakeToCamel(name);
35
36
  }
36
37
 
37
38
  function typeName(name: string): string {
@@ -43,7 +44,7 @@ function quote(value: string): string {
43
44
  }
44
45
 
45
46
  function rowAccessor(column: QueryColumn): string {
46
- const key = `row[${quote(column.name)}]`;
47
+ const key = `row[${quote(column.langName)}]`;
47
48
  switch (column.type) {
48
49
  case 'string':
49
50
  case 'json':
@@ -72,6 +73,46 @@ function paramValue(type: IrColumnType, name: string): string {
72
73
  }
73
74
  }
74
75
 
76
+ /** Bind for a §4 OPTIONAL param: Dart `null` rides as JSON null (the §7
77
+ * neutralization guards make it a no-op). */
78
+ function optionalParamValue(type: IrColumnType, name: string): string {
79
+ switch (type) {
80
+ case 'bytes':
81
+ case 'crdt':
82
+ return `${name} == null ? null : _queryBindBytes(${name})`;
83
+ default:
84
+ return name;
85
+ }
86
+ }
87
+
88
+ function isOptionalParam(query: AnalyzedQuery, p: QueryParam): boolean {
89
+ return (
90
+ p.optional === true ||
91
+ p.flag === true ||
92
+ (query.limit !== undefined && p.name === 'limit')
93
+ );
94
+ }
95
+
96
+ /** Per-query orderBy allowlist enum (column = the checked SQL column). */
97
+ function emitOrderByEnum(query: AnalyzedQuery): string[] {
98
+ if (query.orderBy === undefined) return [];
99
+ const lines: string[] = [];
100
+ lines.push(
101
+ `/// §6 orderBy allowlist for ${query.name} — checked at generate time.`,
102
+ );
103
+ lines.push(`enum ${typeName(query.name)}OrderBy {`);
104
+ lines.push(
105
+ query.orderBy.allowed
106
+ .map((col) => ` ${camelCase(col.langName)}(${quote(col.name)})`)
107
+ .join(',\n') + ';',
108
+ );
109
+ lines.push('');
110
+ lines.push(` const ${typeName(query.name)}OrderBy(this.column);`);
111
+ lines.push(' final String column;');
112
+ lines.push('}');
113
+ return lines;
114
+ }
115
+
75
116
  function emitClass(query: AnalyzedQuery): string[] {
76
117
  const Row = `${typeName(query.name)}Row`;
77
118
  const lines: string[] = [];
@@ -80,26 +121,26 @@ function emitClass(query: AnalyzedQuery): string[] {
80
121
  for (const column of query.columns) {
81
122
  const opt = column.nullable ? '?' : '';
82
123
  lines.push(
83
- ` final ${DART_TYPE[column.type]}${opt} ${camelCase(column.name)};`,
124
+ ` final ${DART_TYPE[column.type]}${opt} ${camelCase(column.langName)};`,
84
125
  );
85
126
  }
86
127
  lines.push('');
87
128
  const ctorParams = query.columns
88
- .map((c) => `${c.nullable ? '' : 'required '}this.${camelCase(c.name)}`)
129
+ .map((c) => `${c.nullable ? '' : 'required '}this.${camelCase(c.langName)}`)
89
130
  .join(', ');
90
131
  lines.push(` const ${Row}({${ctorParams}});`);
91
132
  lines.push('');
92
133
  lines.push(` static ${Row}? fromRow(Map<String, Object?> row) {`);
93
134
  for (const column of query.columns) {
94
135
  if (!column.nullable) {
95
- const name = camelCase(column.name);
136
+ const name = camelCase(column.langName);
96
137
  lines.push(` final ${name} = ${rowAccessor(column)};`);
97
138
  lines.push(` if (${name} == null) return null;`);
98
139
  }
99
140
  }
100
141
  const args = query.columns
101
142
  .map((c) => {
102
- const name = camelCase(c.name);
143
+ const name = camelCase(c.langName);
103
144
  return c.nullable ? `${name}: ${rowAccessor(c)}` : `${name}: ${name}`;
104
145
  })
105
146
  .join(', ');
@@ -120,27 +161,68 @@ function emitRunner(query: AnalyzedQuery): string[] {
120
161
  `const List<String> syncular${Pascal}QueryTables = [${query.tables.map(quote).join(', ')}];`,
121
162
  );
122
163
  lines.push('');
123
- lines.push(`const String _${query.name}Sql = ${quote(query.positionalSql)};`);
164
+ if (query.orderBy !== undefined) {
165
+ lines.push(
166
+ `const String _${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')};`,
167
+ );
168
+ } else {
169
+ lines.push(
170
+ `const String _${query.name}Sql = ${quote(query.positionalSql)};`,
171
+ );
172
+ }
124
173
  lines.push('');
125
174
  lines.push(`/// Run the ${query.name} named query (SELECT-only).`);
126
- const args = query.params
127
- .map((p) => `required ${DART_TYPE[p.type]} ${camelCase(p.name)}`)
128
- .join(', ');
129
- const argsClause = query.params.length > 0 ? `, {${args}}` : '';
175
+ const args: string[] = [];
176
+ for (const p of query.params) {
177
+ const name = camelCase(p.langName);
178
+ if (isOptionalParam(query, p)) {
179
+ args.push(`${DART_TYPE[p.type]}? ${name}`);
180
+ } else {
181
+ args.push(`required ${DART_TYPE[p.type]} ${name}`);
182
+ }
183
+ }
184
+ if (query.orderBy !== undefined) {
185
+ const defaultCase = camelCase(
186
+ query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
187
+ ?.langName ?? query.orderBy.defaultColumn,
188
+ );
189
+ args.push(
190
+ `${typeName(query.name)}OrderBy orderBy = ${typeName(query.name)}OrderBy.${defaultCase}`,
191
+ );
192
+ args.push(
193
+ `SyncularQueryDir dir = SyncularQueryDir.${query.orderBy.defaultDir}`,
194
+ );
195
+ }
196
+ const argsClause = args.length > 0 ? `, {${args.join(', ')}}` : '';
130
197
  lines.push(
131
198
  `List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`,
132
199
  );
200
+ if (query.orderBy !== undefined) {
201
+ const limitTail =
202
+ query.positionalLimitTail !== undefined
203
+ ? ` ${quote(query.positionalLimitTail.trim())}`
204
+ : '';
205
+ lines.push(
206
+ ` final sql = '$_${query.name}SqlBase order by \${orderBy.column} \${dir.name}'${limitTail === '' ? '' : `\n ' ' ${limitTail.trim()}`};`,
207
+ );
208
+ }
209
+ const sqlRef = query.orderBy !== undefined ? 'sql' : `_${query.name}Sql`;
133
210
  if (query.params.length > 0) {
134
211
  const binds = query.params
135
- .map((p) => paramValue(p.type, camelCase(p.name)))
212
+ .map((p) => {
213
+ const name = camelCase(p.langName);
214
+ return isOptionalParam(query, p)
215
+ ? optionalParamValue(p.type, name)
216
+ : paramValue(p.type, name);
217
+ })
136
218
  .join(', ');
137
219
  lines.push(` final params = <Object?>[${binds}];`);
138
220
  lines.push(
139
- ` return client.query(_${query.name}Sql, params: params).map(${Row}.fromRow).whereType<${Row}>().toList();`,
221
+ ` return client.query(${sqlRef}, params: params).map(${Row}.fromRow).whereType<${Row}>().toList();`,
140
222
  );
141
223
  } else {
142
224
  lines.push(
143
- ` return client.query(_${query.name}Sql).map(${Row}.fromRow).whereType<${Row}>().toList();`,
225
+ ` return client.query(${sqlRef}).map(${Row}.fromRow).whereType<${Row}>().toList();`,
144
226
  );
145
227
  }
146
228
  lines.push('}');
@@ -192,8 +274,19 @@ export function emitQueriesDartModule(
192
274
  ].join('\n'),
193
275
  );
194
276
 
277
+ if (queries.some((q) => q.orderBy !== undefined)) {
278
+ parts.push(
279
+ [
280
+ '/// §6 orderBy direction (shared by every orderBy-knob query).',
281
+ 'enum SyncularQueryDir { asc, desc }',
282
+ ].join('\n'),
283
+ );
284
+ }
285
+
195
286
  for (const query of queries) {
196
287
  parts.push(emitClass(query).join('\n'));
288
+ const orderByEnum = emitOrderByEnum(query);
289
+ if (orderByEnum.length > 0) parts.push(orderByEnum.join('\n'));
197
290
  }
198
291
  for (const query of queries) {
199
292
  parts.push(emitRunner(query).join('\n'));