turbine-orm 0.75.0 → 0.76.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 +48 -7
- package/dist/cjs/cli/compile-query.d.ts +22 -2
- package/dist/cjs/cli/compile-query.js +49 -5
- package/dist/cjs/cli/config.d.ts +2 -0
- package/dist/cjs/cli/config.js +1 -1
- package/dist/cjs/cli/destructive.js +78 -43
- package/dist/cjs/cli/index.d.ts +95 -1
- package/dist/cjs/cli/index.js +609 -145
- package/dist/cjs/cli/mcp.js +30 -1
- package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cjs/cli/pii-predicate-guard.js +72 -12
- package/dist/cjs/cli/rate-limit.js +38 -1
- package/dist/cjs/cli/studio.js +26 -5
- package/dist/cjs/cli/ui.d.ts +33 -0
- package/dist/cjs/cli/ui.js +53 -7
- package/dist/cjs/client.d.ts +13 -1
- package/dist/cjs/client.js +1 -1
- package/dist/cjs/errors.d.ts +12 -1
- package/dist/cjs/errors.js +11 -2
- package/dist/cjs/generate.d.ts +26 -0
- package/dist/cjs/generate.js +174 -27
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/introspect.d.ts +17 -0
- package/dist/cjs/introspect.js +100 -1
- package/dist/cjs/mssql.d.ts +18 -0
- package/dist/cjs/mssql.js +20 -1
- package/dist/cjs/pipeline.js +44 -6
- package/dist/cjs/powql.js +51 -17
- package/dist/cjs/query/batched-loader.js +3 -3
- package/dist/cjs/query/builder.js +1 -1
- package/dist/cjs/query/relations.d.ts +5 -0
- package/dist/cjs/query/relations.js +141 -69
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +16 -0
- package/dist/cjs/serverless.d.ts +1 -1
- package/dist/cjs/serverless.js +1 -1
- package/dist/cjs/sqlite.d.ts +33 -1
- package/dist/cjs/sqlite.js +84 -3
- package/dist/cli/compile-query.d.ts +22 -2
- package/dist/cli/compile-query.js +50 -6
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/destructive.js +78 -43
- package/dist/cli/index.d.ts +95 -1
- package/dist/cli/index.js +604 -147
- package/dist/cli/mcp.js +30 -1
- package/dist/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cli/pii-predicate-guard.js +73 -13
- package/dist/cli/rate-limit.js +38 -1
- package/dist/cli/studio.js +27 -6
- package/dist/cli/ui.d.ts +33 -0
- package/dist/cli/ui.js +51 -7
- package/dist/client.d.ts +13 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +11 -2
- package/dist/generate.d.ts +26 -0
- package/dist/generate.js +172 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +17 -0
- package/dist/introspect.js +98 -1
- package/dist/mssql.d.ts +18 -0
- package/dist/mssql.js +20 -1
- package/dist/pipeline.js +44 -6
- package/dist/powql.js +53 -19
- package/dist/query/batched-loader.js +4 -4
- package/dist/query/builder.js +2 -2
- package/dist/query/relations.d.ts +5 -0
- package/dist/query/relations.js +141 -70
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +15 -0
- package/dist/serverless.d.ts +1 -1
- package/dist/serverless.js +1 -1
- package/dist/sqlite.d.ts +33 -1
- package/dist/sqlite.js +85 -4
- package/package.json +2 -2
package/dist/generate.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
|
+
import { ValidationError } from './errors.js';
|
|
13
14
|
import { pgTypeToTs, singularize, snakeToPascal, timeOfDayKind, withDbFieldNames, } from './schema.js';
|
|
14
15
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
15
16
|
function entityName(tableName) {
|
|
@@ -57,9 +58,59 @@ function writeColumnTsType(col, enums) {
|
|
|
57
58
|
const widened = isArray ? '(string | Date)[]' : 'string | Date';
|
|
58
59
|
return col.nullable ? `${widened} | null` : widened;
|
|
59
60
|
}
|
|
60
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Characters that must never reach generated source verbatim: the two that end
|
|
63
|
+
* a single-quoted literal (backslash, quote), the C0/C1 control range (a raw
|
|
64
|
+
* newline alone leaves the literal unterminated and starts a new source line),
|
|
65
|
+
* and the two Unicode line terminators.
|
|
66
|
+
*/
|
|
67
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: neutralizing control characters is this pattern's purpose
|
|
68
|
+
const UNSAFE_EMIT_CHARS_GLOBAL = /[\\'\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
69
|
+
/**
|
|
70
|
+
* Escape a value for embedding in a single-quoted TypeScript string literal.
|
|
71
|
+
*
|
|
72
|
+
* Every character that can END the literal has to be neutralized here, not just
|
|
73
|
+
* the quote. A database identifier is attacker-controlled text (Postgres allows
|
|
74
|
+
* any character in a double-quoted name, up to 63 bytes) and generated code is
|
|
75
|
+
* `import`ed, i.e. EXECUTED, so a literal that closes early leaves the rest of
|
|
76
|
+
* the name in expression position. A raw newline is enough on its own.
|
|
77
|
+
*/
|
|
61
78
|
function escSQ(value) {
|
|
62
|
-
return value.replace(
|
|
79
|
+
return value.replace(UNSAFE_EMIT_CHARS_GLOBAL, (ch) => {
|
|
80
|
+
switch (ch) {
|
|
81
|
+
case '\\':
|
|
82
|
+
return '\\\\';
|
|
83
|
+
case "'":
|
|
84
|
+
return "\\'";
|
|
85
|
+
case '\n':
|
|
86
|
+
return '\\n';
|
|
87
|
+
case '\r':
|
|
88
|
+
return '\\r';
|
|
89
|
+
case '\t':
|
|
90
|
+
return '\\t';
|
|
91
|
+
case '\b':
|
|
92
|
+
return '\\b';
|
|
93
|
+
case '\f':
|
|
94
|
+
return '\\f';
|
|
95
|
+
case '\v':
|
|
96
|
+
return '\\v';
|
|
97
|
+
default:
|
|
98
|
+
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: collapsing control characters is this pattern's purpose
|
|
103
|
+
const COMMENT_BREAKING_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g;
|
|
104
|
+
/**
|
|
105
|
+
* Neutralize a catalog string for emission inside a generated JSDoc or line
|
|
106
|
+
* COMMENT. Two sequences end a comment early and both are legal in a
|
|
107
|
+
* double-quoted Postgres identifier: an asterisk followed by a slash closes a
|
|
108
|
+
* block comment, putting everything after it in code position, and a newline
|
|
109
|
+
* ends a line comment and splits one emitted line into two. Escaping the
|
|
110
|
+
* asterisk breaks the terminator without changing how the name reads.
|
|
111
|
+
*/
|
|
112
|
+
function docSafe(value) {
|
|
113
|
+
return value.replace(/\*\//g, '*\\/').replace(COMMENT_BREAKING_CHARS, ' ');
|
|
63
114
|
}
|
|
64
115
|
// ---------------------------------------------------------------------------
|
|
65
116
|
// Main generate function
|
|
@@ -284,12 +335,75 @@ function typeSafeRelations(table, warn = true) {
|
|
|
284
335
|
}
|
|
285
336
|
return usable;
|
|
286
337
|
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Identifier boundary
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
/**
|
|
342
|
+
* The JavaScript identifier grammar as a WHOLE-STRING match: `IdentifierStart`
|
|
343
|
+
* (`ID_Start`, `$`, `_`) followed by `IdentifierPart` (`ID_Continue`, `$`, ZWNJ,
|
|
344
|
+
* ZWJ). Deliberately Unicode-aware rather than `[A-Za-z_$][\w$]*`: a table named
|
|
345
|
+
* `café` yields the perfectly valid identifier `Café` and there is no reason to
|
|
346
|
+
* refuse it. What it does refuse is every character that could end the
|
|
347
|
+
* identifier token, so a name that passes interpolates as exactly one token.
|
|
348
|
+
*/
|
|
349
|
+
const EMITTABLE_IDENTIFIER_RE = /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u;
|
|
350
|
+
/**
|
|
351
|
+
* Whether `name` can be interpolated into generated TypeScript in IDENTIFIER
|
|
352
|
+
* position (a type name, an interface name, a class member declaration).
|
|
353
|
+
*
|
|
354
|
+
* Identifier position is the one emission site with no escaping option: a value
|
|
355
|
+
* position becomes a quoted literal ({@link escSQ}), a key position becomes a
|
|
356
|
+
* quoted key ({@link quoteIfNeeded}), a comment is neutralized
|
|
357
|
+
* ({@link docSafe}), but `export interface X` needs a real identifier. So the
|
|
358
|
+
* only sound answer for a name that is not one is to refuse.
|
|
359
|
+
*/
|
|
360
|
+
export function isEmittableIdentifier(name) {
|
|
361
|
+
return EMITTABLE_IDENTIFIER_RE.test(name);
|
|
362
|
+
}
|
|
363
|
+
function requireEmittable(derived, subject, role) {
|
|
364
|
+
if (isEmittableIdentifier(derived))
|
|
365
|
+
return;
|
|
366
|
+
throw new ValidationError(`[turbine] Cannot generate code for ${subject}: it produces ${JSON.stringify(derived)} as ${role}, ` +
|
|
367
|
+
`which is not a valid TypeScript identifier. Rename the database object, or exclude it from generation ` +
|
|
368
|
+
`(introspect \`exclude\`).`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Refuse a schema whose names cannot be emitted as TypeScript identifiers.
|
|
372
|
+
*
|
|
373
|
+
* Called by every emitter that puts a catalog-derived name in identifier
|
|
374
|
+
* position ({@link generateTypes}, {@link generateIndex}, {@link generateZod}).
|
|
375
|
+
* {@link generateMetadata} deliberately does NOT call it: metadata.ts emits no
|
|
376
|
+
* identifiers derived from catalog names, every name there is a quoted key or a
|
|
377
|
+
* quoted value, so it has no identifier rule to enforce and stays usable for a
|
|
378
|
+
* schema whose type layer cannot be generated.
|
|
379
|
+
*
|
|
380
|
+
* The check runs on the DERIVED identifier, not the raw name, because that is
|
|
381
|
+
* what actually lands in the output, but the message names the raw object so
|
|
382
|
+
* the reader knows what to rename.
|
|
383
|
+
*/
|
|
384
|
+
export function assertEmittableSchema(schema) {
|
|
385
|
+
for (const enumName of Object.keys(schema.enums)) {
|
|
386
|
+
requireEmittable(snakeToPascal(enumName), `enum type "${enumName}"`, 'the generated enum type name');
|
|
387
|
+
}
|
|
388
|
+
for (const table of Object.values(schema.tables)) {
|
|
389
|
+
requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
|
|
390
|
+
requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
|
|
391
|
+
// Only the relations that reach the TYPE layer: a relation shadowing a
|
|
392
|
+
// column field is already dropped from types.ts, so refusing on its name
|
|
393
|
+
// would refuse a schema that generates fine.
|
|
394
|
+
for (const [relName, rel] of typeSafeRelations(table, false)) {
|
|
395
|
+
requireEmittable(snakeToPascal(relName), `relation "${relName}" on table "${table.name}"`, 'part of the generated `XWithY` interface name');
|
|
396
|
+
requireEmittable(entityName(rel.to), `relation "${relName}" on table "${table.name}" (target "${rel.to}")`, 'the generated target entity type name');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
287
400
|
/**
|
|
288
401
|
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
289
402
|
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
290
403
|
* generator output without writing files to disk.
|
|
291
404
|
*/
|
|
292
405
|
export function generateTypes(schema, options) {
|
|
406
|
+
assertEmittableSchema(schema);
|
|
293
407
|
const lines = [...generatedFileHeader(options)];
|
|
294
408
|
// We import UpdateOperatorInput so generated *Update types can express
|
|
295
409
|
// atomic increment / decrement / multiply / divide / set operators on
|
|
@@ -320,7 +434,7 @@ export function generateTypes(schema, options) {
|
|
|
320
434
|
// Generate enum types
|
|
321
435
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
322
436
|
const typeName = snakeToPascal(enumName);
|
|
323
|
-
lines.push(`/** Database enum: ${enumName} */`);
|
|
437
|
+
lines.push(`/** Database enum: ${docSafe(enumName)} */`);
|
|
324
438
|
lines.push(`export type ${typeName} = ${labels.map((l) => `'${escSQ(l)}'`).join(' | ')};`);
|
|
325
439
|
lines.push('');
|
|
326
440
|
}
|
|
@@ -328,7 +442,7 @@ export function generateTypes(schema, options) {
|
|
|
328
442
|
for (const table of Object.values(schema.tables)) {
|
|
329
443
|
const typeName = entityName(table.name);
|
|
330
444
|
// --- Base entity interface ---
|
|
331
|
-
lines.push(`/** Row type for the \`${table.name}\` table */`);
|
|
445
|
+
lines.push(`/** Row type for the \`${docSafe(table.name)}\` table */`);
|
|
332
446
|
lines.push(`export interface ${typeName} {`);
|
|
333
447
|
for (const col of table.columns) {
|
|
334
448
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
@@ -338,7 +452,7 @@ export function generateTypes(schema, options) {
|
|
|
338
452
|
// The emitted type marks it optional so it tells the truth about absence.
|
|
339
453
|
const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
|
|
340
454
|
const optional = col.pii ? '?' : '';
|
|
341
|
-
lines.push(` /** Column: ${col.name}, ${col.pgType}${pkNote}${nullNote}${piiNote} */`);
|
|
455
|
+
lines.push(` /** Column: ${docSafe(col.name)}, ${docSafe(col.pgType)}${pkNote}${nullNote}${piiNote} */`);
|
|
342
456
|
lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${columnTsType(col, schema.enums)};`);
|
|
343
457
|
}
|
|
344
458
|
lines.push('}');
|
|
@@ -346,7 +460,7 @@ export function generateTypes(schema, options) {
|
|
|
346
460
|
// --- Create input type ---
|
|
347
461
|
// Required: non-nullable columns without defaults (except PK)
|
|
348
462
|
// Optional: nullable columns (default to NULL) or columns with explicit defaults
|
|
349
|
-
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
463
|
+
lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
|
|
350
464
|
lines.push(`export type ${typeName}Create = {`);
|
|
351
465
|
for (const col of table.columns) {
|
|
352
466
|
// STORED generated columns are computed by the database, never writable.
|
|
@@ -369,7 +483,7 @@ export function generateTypes(schema, options) {
|
|
|
369
483
|
// Numeric columns additionally accept `UpdateOperatorInput<number>` so
|
|
370
484
|
// users can write `{ viewCount: { increment: 1 } }` without an `as any`.
|
|
371
485
|
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
|
|
372
|
-
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
486
|
+
lines.push(`/** Input type for updating a row in \`${docSafe(table.name)}\` */`);
|
|
373
487
|
lines.push(`export type ${typeName}Update = {`);
|
|
374
488
|
for (const col of nonPkCols) {
|
|
375
489
|
lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(writeColumnTsType(col, schema.enums))};`);
|
|
@@ -387,14 +501,14 @@ export function generateTypes(schema, options) {
|
|
|
387
501
|
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
388
502
|
const hasRelations = safeRelations.length > 0;
|
|
389
503
|
if (hasRelations) {
|
|
390
|
-
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
504
|
+
lines.push(`/** Available relations for the \`${docSafe(table.name)}\` table */`);
|
|
391
505
|
lines.push(`export interface ${typeName}Relations {`);
|
|
392
506
|
for (const [relName, rel] of safeRelations) {
|
|
393
507
|
const targetType = entityName(rel.to);
|
|
394
508
|
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
395
509
|
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
396
510
|
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
397
|
-
lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
511
|
+
lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
398
512
|
}
|
|
399
513
|
lines.push('}');
|
|
400
514
|
lines.push('');
|
|
@@ -402,15 +516,15 @@ export function generateTypes(schema, options) {
|
|
|
402
516
|
for (const [relName, rel] of safeRelations) {
|
|
403
517
|
const targetType = entityName(rel.to);
|
|
404
518
|
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
405
|
-
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
519
|
+
lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
|
|
406
520
|
lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
|
|
407
|
-
lines.push(` ${relName}: ${targetType}[];`);
|
|
521
|
+
lines.push(` ${quoteIfNeeded(relName)}: ${targetType}[];`);
|
|
408
522
|
lines.push('}');
|
|
409
523
|
}
|
|
410
524
|
else {
|
|
411
|
-
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
525
|
+
lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
|
|
412
526
|
lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
|
|
413
|
-
lines.push(` ${relName}: ${targetType} | null;`);
|
|
527
|
+
lines.push(` ${quoteIfNeeded(relName)}: ${targetType} | null;`);
|
|
414
528
|
lines.push('}');
|
|
415
529
|
}
|
|
416
530
|
lines.push('');
|
|
@@ -507,7 +621,7 @@ export function generateTypes(schema, options) {
|
|
|
507
621
|
lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
|
|
508
622
|
for (const [relName, rel] of safeRelations) {
|
|
509
623
|
const targetType = entityName(rel.to);
|
|
510
|
-
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
624
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
|
|
511
625
|
}
|
|
512
626
|
lines.push('};');
|
|
513
627
|
lines.push('');
|
|
@@ -515,10 +629,10 @@ export function generateTypes(schema, options) {
|
|
|
515
629
|
for (const [relName, rel] of safeRelations) {
|
|
516
630
|
const targetType = entityName(rel.to);
|
|
517
631
|
if (rel.type === 'hasMany') {
|
|
518
|
-
lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
|
|
632
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedUpdateInput;`);
|
|
519
633
|
}
|
|
520
634
|
else {
|
|
521
|
-
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
635
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
|
|
522
636
|
}
|
|
523
637
|
}
|
|
524
638
|
lines.push('};');
|
|
@@ -623,6 +737,7 @@ function zodBaseType(col, enums, forWrite = false) {
|
|
|
623
737
|
* the output without writing files.
|
|
624
738
|
*/
|
|
625
739
|
export function generateZod(schema, options) {
|
|
740
|
+
assertEmittableSchema(schema);
|
|
626
741
|
const lines = [...generatedFileHeader(options)];
|
|
627
742
|
// `zod` is a USER dependency, this generated file imports it, but the Turbine
|
|
628
743
|
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
@@ -631,7 +746,7 @@ export function generateZod(schema, options) {
|
|
|
631
746
|
for (const table of Object.values(schema.tables)) {
|
|
632
747
|
const typeName = entityName(table.name);
|
|
633
748
|
// Full-row schema.
|
|
634
|
-
lines.push(`/** Zod schema for a \`${table.name}\` row */`);
|
|
749
|
+
lines.push(`/** Zod schema for a \`${docSafe(table.name)}\` row */`);
|
|
635
750
|
lines.push(`export const ${typeName}Schema = z.object({`);
|
|
636
751
|
for (const col of table.columns) {
|
|
637
752
|
let expr = zodBaseType(col, schema.enums);
|
|
@@ -643,7 +758,7 @@ export function generateZod(schema, options) {
|
|
|
643
758
|
lines.push('');
|
|
644
759
|
// Create schema, STORED generated columns can never be written; PK,
|
|
645
760
|
// defaulted, and nullable columns are optional.
|
|
646
|
-
lines.push(`/** Zod schema for creating a \`${table.name}\` row */`);
|
|
761
|
+
lines.push(`/** Zod schema for creating a \`${docSafe(table.name)}\` row */`);
|
|
647
762
|
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
648
763
|
for (const col of table.columns) {
|
|
649
764
|
if (col.isGeneratedStored)
|
|
@@ -659,7 +774,7 @@ export function generateZod(schema, options) {
|
|
|
659
774
|
lines.push('});');
|
|
660
775
|
lines.push('');
|
|
661
776
|
// Update schema, PK and STORED generated columns omitted; all else optional.
|
|
662
|
-
lines.push(`/** Zod schema for updating a \`${table.name}\` row */`);
|
|
777
|
+
lines.push(`/** Zod schema for updating a \`${docSafe(table.name)}\` row */`);
|
|
663
778
|
lines.push(`export const ${typeName}UpdateSchema = z.object({`);
|
|
664
779
|
for (const col of table.columns) {
|
|
665
780
|
if (col.isGeneratedStored)
|
|
@@ -689,7 +804,7 @@ export function generateMetadata(schema, options) {
|
|
|
689
804
|
' tables: {',
|
|
690
805
|
];
|
|
691
806
|
for (const table of Object.values(schema.tables)) {
|
|
692
|
-
lines.push(` ${table.name}: {`);
|
|
807
|
+
lines.push(` ${quoteIfNeeded(table.name)}: {`);
|
|
693
808
|
lines.push(` name: '${escSQ(table.name)}',`);
|
|
694
809
|
// columns
|
|
695
810
|
lines.push(' columns: [');
|
|
@@ -750,7 +865,7 @@ export function generateMetadata(schema, options) {
|
|
|
750
865
|
`sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
|
|
751
866
|
`targetKey: ${keyLiteral(rel.through.targetKey)} }`;
|
|
752
867
|
}
|
|
753
|
-
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
868
|
+
lines.push(` ${quoteIfNeeded(relName)}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
754
869
|
}
|
|
755
870
|
lines.push(' },');
|
|
756
871
|
// indexes
|
|
@@ -783,7 +898,7 @@ export function generateMetadata(schema, options) {
|
|
|
783
898
|
// enums
|
|
784
899
|
lines.push(' enums: {');
|
|
785
900
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
786
|
-
lines.push(` ${enumName}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
901
|
+
lines.push(` ${quoteIfNeeded(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
787
902
|
}
|
|
788
903
|
lines.push(' },');
|
|
789
904
|
lines.push('};');
|
|
@@ -798,6 +913,7 @@ export function generateMetadata(schema, options) {
|
|
|
798
913
|
// index.ts generator (configured client with typed table accessors)
|
|
799
914
|
// ---------------------------------------------------------------------------
|
|
800
915
|
export function generateIndex(schema, options) {
|
|
916
|
+
assertEmittableSchema(schema);
|
|
801
917
|
const tableEntries = Object.values(schema.tables);
|
|
802
918
|
// Must mirror generateTypes: `XRelations` only exists in types.ts when the
|
|
803
919
|
// table has at least one type-safe (non-column-shadowing) relation.
|
|
@@ -844,7 +960,7 @@ export function generateIndex(schema, options) {
|
|
|
844
960
|
const accessor = snakeToCamelStr(table.name);
|
|
845
961
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
846
962
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
847
|
-
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
963
|
+
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table (transaction-scoped) */`);
|
|
848
964
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
849
965
|
}
|
|
850
966
|
lines.push('}');
|
|
@@ -867,7 +983,7 @@ export function generateIndex(schema, options) {
|
|
|
867
983
|
lines.push(' *');
|
|
868
984
|
lines.push(' * Tables:');
|
|
869
985
|
for (const table of tableEntries) {
|
|
870
|
-
lines.push(` * - \`${snakeToCamelStr(table.name)}\` (${table.name})`);
|
|
986
|
+
lines.push(` * - \`${docSafe(snakeToCamelStr(table.name))}\` (${docSafe(table.name)})`);
|
|
871
987
|
}
|
|
872
988
|
lines.push(' *');
|
|
873
989
|
lines.push(' * @example');
|
|
@@ -876,7 +992,7 @@ export function generateIndex(schema, options) {
|
|
|
876
992
|
if (tableEntries.length > 0) {
|
|
877
993
|
const firstTable = tableEntries[0];
|
|
878
994
|
const accessor = snakeToCamelStr(firstTable.name);
|
|
879
|
-
lines.push(` * const rows = await db.${accessor}.findMany();`);
|
|
995
|
+
lines.push(` * const rows = await db.${docSafe(accessor)}.findMany();`);
|
|
880
996
|
}
|
|
881
997
|
lines.push(' * ```');
|
|
882
998
|
lines.push(' */');
|
|
@@ -886,7 +1002,7 @@ export function generateIndex(schema, options) {
|
|
|
886
1002
|
const accessor = snakeToCamelStr(table.name);
|
|
887
1003
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
888
1004
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
889
|
-
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
1005
|
+
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table */`);
|
|
890
1006
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
891
1007
|
}
|
|
892
1008
|
lines.push('');
|
|
@@ -1061,8 +1177,37 @@ function serializeColumn(col) {
|
|
|
1061
1177
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
1062
1178
|
return `{ ${parts.join(', ')} }`;
|
|
1063
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* The subset of names emitted BARE in generated object-key position. Anchored
|
|
1182
|
+
* on purpose: the previous rule only tested whether a name contained a
|
|
1183
|
+
* character outside the set, so a name made entirely of allowed characters but
|
|
1184
|
+
* starting with a digit (`2fa`) was emitted bare and did not parse.
|
|
1185
|
+
*/
|
|
1186
|
+
const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1187
|
+
/**
|
|
1188
|
+
* Emit `s` in generated object-KEY position.
|
|
1189
|
+
*
|
|
1190
|
+
* A key is the one place in the output where catalog text would otherwise land
|
|
1191
|
+
* in EXPRESSION position: `{ [expr]: v }` is a COMPUTED key, evaluated when the
|
|
1192
|
+
* object is constructed, so an unquoted name carrying brackets runs on
|
|
1193
|
+
* `import`. The bare form is therefore gated on a WHOLE-STRING match of the
|
|
1194
|
+
* identifier grammar, which no bracket, parenthesis, quote, newline, or space
|
|
1195
|
+
* can pass; every other name becomes a fully escaped string literal, which is
|
|
1196
|
+
* inert in key position.
|
|
1197
|
+
*
|
|
1198
|
+
* The literal has to be ESCAPED, not merely wrapped: this function used to
|
|
1199
|
+
* return `'${s}'` verbatim, so a name containing a quote closed the key and
|
|
1200
|
+
* reopened in expression position.
|
|
1201
|
+
*
|
|
1202
|
+
* KNOWN LIMIT, deliberately not handled here: a database object named
|
|
1203
|
+
* `__proto__` still sets the emitted object literal's PROTOTYPE rather than
|
|
1204
|
+
* adding a property, because the object-literal special case applies to the
|
|
1205
|
+
* quoted spelling too. Quoting is not a fix for it, so there is no branch for
|
|
1206
|
+
* it; the consequence is a metadata map that silently omits that one entry, not
|
|
1207
|
+
* code execution.
|
|
1208
|
+
*/
|
|
1064
1209
|
function quoteIfNeeded(s) {
|
|
1065
|
-
return
|
|
1210
|
+
return BARE_KEY_RE.test(s) ? s : `'${escSQ(s)}'`;
|
|
1066
1211
|
}
|
|
1067
1212
|
function snakeToCamelStr(s) {
|
|
1068
1213
|
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* // npx turbine generate
|
|
11
11
|
*
|
|
12
12
|
* // 2. Import the generated client:
|
|
13
|
-
* import { turbine } from './generated/turbine';
|
|
13
|
+
* import { turbine } from './generated/turbine/index.js';
|
|
14
14
|
*
|
|
15
15
|
* const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
16
16
|
*
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* // npx turbine generate
|
|
11
11
|
*
|
|
12
12
|
* // 2. Import the generated client:
|
|
13
|
-
* import { turbine } from './generated/turbine';
|
|
13
|
+
* import { turbine } from './generated/turbine/index.js';
|
|
14
14
|
*
|
|
15
15
|
* const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
16
16
|
*
|
package/dist/introspect.d.ts
CHANGED
|
@@ -105,6 +105,23 @@ export interface IntrospectOptions {
|
|
|
105
105
|
*/
|
|
106
106
|
dialect?: Dialect;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Refuse one catalog identifier carrying a character from
|
|
110
|
+
* {@link UNSAFE_CATALOG_CHARS}. `subject` names the object in the error.
|
|
111
|
+
*/
|
|
112
|
+
export declare function assertSafeCatalogIdentifier(name: string, subject: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Walk a freshly introspected {@link SchemaMetadata} and refuse any catalog
|
|
115
|
+
* identifier that carries a control character (see
|
|
116
|
+
* {@link assertSafeCatalogIdentifier}).
|
|
117
|
+
*
|
|
118
|
+
* Covers every string that is a NAME: tables, columns (catalog name and derived
|
|
119
|
+
* field), relations (key, name, endpoints, keys, junction), enum types and their
|
|
120
|
+
* labels, index names and their columns, and check-constraint names. It does
|
|
121
|
+
* NOT cover free-text SQL, index definitions, check expressions, and column
|
|
122
|
+
* defaults are emitted with `JSON.stringify` and are not identifiers.
|
|
123
|
+
*/
|
|
124
|
+
export declare function assertSafeCatalogSchema(schema: SchemaMetadata): void;
|
|
108
125
|
/**
|
|
109
126
|
* Introspect a database into {@link SchemaMetadata}, routing through the active
|
|
110
127
|
* dialect's {@link Dialect.introspector} so each engine can override the catalog
|
package/dist/introspect.js
CHANGED
|
@@ -301,6 +301,99 @@ export function defaultExcludedTablesPresent(names, options = {}) {
|
|
|
301
301
|
// ---------------------------------------------------------------------------
|
|
302
302
|
// Main introspection function
|
|
303
303
|
// ---------------------------------------------------------------------------
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Catalog identifier boundary
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
/**
|
|
308
|
+
* Characters that no legitimate SQL object name carries and that are exactly
|
|
309
|
+
* the primitives for breaking OUT of a generated string literal or comment: the
|
|
310
|
+
* C0 control range (NUL, newline, carriage return, tab), the C1 range, and the
|
|
311
|
+
* two Unicode line terminators.
|
|
312
|
+
*
|
|
313
|
+
* Postgres permits ANY character in a double-quoted identifier up to 63 bytes,
|
|
314
|
+
* so a catalog name is attacker-controlled text as soon as anyone but the DBA
|
|
315
|
+
* can create an object. `turbine generate` turns those names into TypeScript
|
|
316
|
+
* that is then `import`ed, i.e. EXECUTED, and `turbine studio` / the MCP server
|
|
317
|
+
* render them into HTML and JSON. Escaping at each of those sinks is the actual
|
|
318
|
+
* fix (see `escSQ` / `quoteIfNeeded` / `docSafe` in generate.ts); this boundary
|
|
319
|
+
* is the belt-and-braces refusal one layer earlier, and it names the object so
|
|
320
|
+
* the operator can see WHICH one is malformed instead of debugging generated
|
|
321
|
+
* output.
|
|
322
|
+
*
|
|
323
|
+
* Deliberately narrow. It does NOT refuse a name that merely cannot become a
|
|
324
|
+
* TypeScript identifier (`2fa_codes`), because `introspect()` also feeds
|
|
325
|
+
* Studio, the MCP server, and `doctor`, none of which emit identifiers, and
|
|
326
|
+
* refusing there would break tools that work today. That question belongs to
|
|
327
|
+
* the code generator and is answered by `assertEmittableSchema` in generate.ts.
|
|
328
|
+
*/
|
|
329
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this pattern's purpose
|
|
330
|
+
const UNSAFE_CATALOG_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
331
|
+
/**
|
|
332
|
+
* Refuse one catalog identifier carrying a character from
|
|
333
|
+
* {@link UNSAFE_CATALOG_CHARS}. `subject` names the object in the error.
|
|
334
|
+
*/
|
|
335
|
+
export function assertSafeCatalogIdentifier(name, subject) {
|
|
336
|
+
const match = UNSAFE_CATALOG_CHARS.exec(name);
|
|
337
|
+
if (match === null)
|
|
338
|
+
return;
|
|
339
|
+
const code = match[0].charCodeAt(0).toString(16).padStart(4, '0');
|
|
340
|
+
throw new ValidationError(`[turbine] Refusing to introspect ${subject}: its name contains the control character U+${code.toUpperCase()} ` +
|
|
341
|
+
`at position ${match.index}. Such a name cannot be safely emitted into generated code, SQL comments, or ` +
|
|
342
|
+
`tooling output. Rename the database object, or exclude it from introspection.`);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Walk a freshly introspected {@link SchemaMetadata} and refuse any catalog
|
|
346
|
+
* identifier that carries a control character (see
|
|
347
|
+
* {@link assertSafeCatalogIdentifier}).
|
|
348
|
+
*
|
|
349
|
+
* Covers every string that is a NAME: tables, columns (catalog name and derived
|
|
350
|
+
* field), relations (key, name, endpoints, keys, junction), enum types and their
|
|
351
|
+
* labels, index names and their columns, and check-constraint names. It does
|
|
352
|
+
* NOT cover free-text SQL, index definitions, check expressions, and column
|
|
353
|
+
* defaults are emitted with `JSON.stringify` and are not identifiers.
|
|
354
|
+
*/
|
|
355
|
+
export function assertSafeCatalogSchema(schema) {
|
|
356
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
357
|
+
assertSafeCatalogIdentifier(enumName, `enum type "${enumName}"`);
|
|
358
|
+
for (const label of labels) {
|
|
359
|
+
assertSafeCatalogIdentifier(label, `a label of enum type "${enumName}"`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const [tableKey, table] of Object.entries(schema.tables)) {
|
|
363
|
+
assertSafeCatalogIdentifier(tableKey, `table "${tableKey}"`);
|
|
364
|
+
assertSafeCatalogIdentifier(table.name, `table "${tableKey}"`);
|
|
365
|
+
const where = `table "${table.name}"`;
|
|
366
|
+
for (const col of table.columns) {
|
|
367
|
+
assertSafeCatalogIdentifier(col.name, `column "${col.name}" on ${where}`);
|
|
368
|
+
assertSafeCatalogIdentifier(col.field, `the field name derived for a column on ${where}`);
|
|
369
|
+
}
|
|
370
|
+
for (const [relKey, rel] of Object.entries(table.relations)) {
|
|
371
|
+
const relWhere = `relation "${relKey}" on ${where}`;
|
|
372
|
+
assertSafeCatalogIdentifier(relKey, relWhere);
|
|
373
|
+
assertSafeCatalogIdentifier(rel.name, relWhere);
|
|
374
|
+
assertSafeCatalogIdentifier(rel.from, `the source table of ${relWhere}`);
|
|
375
|
+
assertSafeCatalogIdentifier(rel.to, `the target table of ${relWhere}`);
|
|
376
|
+
for (const k of [rel.foreignKey, rel.referenceKey].flat()) {
|
|
377
|
+
assertSafeCatalogIdentifier(k, `a key column of ${relWhere}`);
|
|
378
|
+
}
|
|
379
|
+
if (rel.through) {
|
|
380
|
+
assertSafeCatalogIdentifier(rel.through.table, `the junction table of ${relWhere}`);
|
|
381
|
+
for (const k of [rel.through.sourceKey, rel.through.targetKey].flat()) {
|
|
382
|
+
assertSafeCatalogIdentifier(k, `a junction key column of ${relWhere}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const idx of table.indexes) {
|
|
387
|
+
assertSafeCatalogIdentifier(idx.name, `index "${idx.name}" on ${where}`);
|
|
388
|
+
for (const c of idx.columns) {
|
|
389
|
+
assertSafeCatalogIdentifier(c, `a column of index "${idx.name}" on ${where}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const chk of table.checks ?? []) {
|
|
393
|
+
assertSafeCatalogIdentifier(chk.name, `check constraint "${chk.name}" on ${where}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
304
397
|
/**
|
|
305
398
|
* Introspect a database into {@link SchemaMetadata}, routing through the active
|
|
306
399
|
* dialect's {@link Dialect.introspector} so each engine can override the catalog
|
|
@@ -314,7 +407,11 @@ export async function introspect(options) {
|
|
|
314
407
|
: // Dialects without an introspector fall back to the Postgres catalog reader.
|
|
315
408
|
await introspectPostgresCatalog(options);
|
|
316
409
|
// Applied here rather than inside each introspector so every engine gets it.
|
|
317
|
-
|
|
410
|
+
const renamed = options.relationNames ? applyRelationRenames(schema, options.relationNames) : schema;
|
|
411
|
+
// Same reason: one boundary for every engine, and AFTER the renames so a
|
|
412
|
+
// caller-supplied relation name is checked too.
|
|
413
|
+
assertSafeCatalogSchema(renamed);
|
|
414
|
+
return renamed;
|
|
318
415
|
}
|
|
319
416
|
/**
|
|
320
417
|
* Rename introspected relations, per table, from the name turbine derived to
|
package/dist/mssql.d.ts
CHANGED
|
@@ -149,6 +149,24 @@ type QueryArg = string | {
|
|
|
149
149
|
export declare class MssqlPool implements PgCompatPool {
|
|
150
150
|
/** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
151
151
|
readonly pool: MssqlConnectionPool;
|
|
152
|
+
/**
|
|
153
|
+
* The dialect this pool speaks, published so a consumer holding only the POOL
|
|
154
|
+
* can find it.
|
|
155
|
+
*
|
|
156
|
+
* `executePipeline` (src/pipeline.ts) is that consumer: it receives a pool and
|
|
157
|
+
* nothing else, and its transaction control used to be the literal strings
|
|
158
|
+
* `BEGIN` / `COMMIT` / `ROLLBACK`. A bare `BEGIN` is a statement-BLOCK opener
|
|
159
|
+
* in T-SQL, so it missed {@link MssqlTxClient}'s `BEGIN TRAN(SACTION)` branch,
|
|
160
|
+
* reached the server as a block with no `END`, and was rejected; the COMMIT
|
|
161
|
+
* and ROLLBACK behind it then found no open transaction and no-oped. Reading
|
|
162
|
+
* the dialect off the pool is what lets the batch emit `BEGIN TRANSACTION`
|
|
163
|
+
* and land on the driver's Transaction API instead.
|
|
164
|
+
*
|
|
165
|
+
* SQL Server is the only engine whose transaction keywords differ from
|
|
166
|
+
* Postgres's, so it is the only pool shim that needs to publish this; the
|
|
167
|
+
* lookup treats an absent `dialect` as PostgreSQL.
|
|
168
|
+
*/
|
|
169
|
+
readonly dialect: Dialect;
|
|
152
170
|
private readonly sqlNS;
|
|
153
171
|
private closed;
|
|
154
172
|
constructor(pool: MssqlConnectionPool, sqlNS: MssqlModule);
|
package/dist/mssql.js
CHANGED
|
@@ -94,6 +94,7 @@ import { postgresDialect, } from './dialect.js';
|
|
|
94
94
|
import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, RelationError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
95
95
|
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
96
96
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
97
|
+
import { availableClause } from './query/utils.js';
|
|
97
98
|
import { camelToSnake, isDateType, normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
98
99
|
// ---------------------------------------------------------------------------
|
|
99
100
|
// SQL Server / connection limits
|
|
@@ -358,6 +359,24 @@ class MssqlTxClient {
|
|
|
358
359
|
export class MssqlPool {
|
|
359
360
|
/** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
360
361
|
pool;
|
|
362
|
+
/**
|
|
363
|
+
* The dialect this pool speaks, published so a consumer holding only the POOL
|
|
364
|
+
* can find it.
|
|
365
|
+
*
|
|
366
|
+
* `executePipeline` (src/pipeline.ts) is that consumer: it receives a pool and
|
|
367
|
+
* nothing else, and its transaction control used to be the literal strings
|
|
368
|
+
* `BEGIN` / `COMMIT` / `ROLLBACK`. A bare `BEGIN` is a statement-BLOCK opener
|
|
369
|
+
* in T-SQL, so it missed {@link MssqlTxClient}'s `BEGIN TRAN(SACTION)` branch,
|
|
370
|
+
* reached the server as a block with no `END`, and was rejected; the COMMIT
|
|
371
|
+
* and ROLLBACK behind it then found no open transaction and no-oped. Reading
|
|
372
|
+
* the dialect off the pool is what lets the batch emit `BEGIN TRANSACTION`
|
|
373
|
+
* and land on the driver's Transaction API instead.
|
|
374
|
+
*
|
|
375
|
+
* SQL Server is the only engine whose transaction keywords differ from
|
|
376
|
+
* Postgres's, so it is the only pool shim that needs to publish this; the
|
|
377
|
+
* lookup treats an absent `dialect` as PostgreSQL.
|
|
378
|
+
*/
|
|
379
|
+
dialect = mssqlDialect;
|
|
361
380
|
sqlNS;
|
|
362
381
|
closed = false;
|
|
363
382
|
constructor(pool, sqlNS) {
|
|
@@ -886,7 +905,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
886
905
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
887
906
|
if (!nestedRelDef) {
|
|
888
907
|
throw new RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
889
|
-
|
|
908
|
+
availableClause(Object.keys(targetMeta.relations), 'It has no relations.'));
|
|
890
909
|
}
|
|
891
910
|
const sub = ctx.recurse(nestedRelDef, nestedSpec, parentAlias, depth + 1, [...path, relDef.name]);
|
|
892
911
|
cols.push(`JSON_QUERY((${sub})) AS ${q(nestedRelName)}`);
|