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/cjs/generate.js
CHANGED
|
@@ -15,6 +15,8 @@ exports.resolveImportExtension = resolveImportExtension;
|
|
|
15
15
|
exports.detectTsconfigExtension = detectTsconfigExtension;
|
|
16
16
|
exports.classifyTsconfig = classifyTsconfig;
|
|
17
17
|
exports.stripJsonComments = stripJsonComments;
|
|
18
|
+
exports.isEmittableIdentifier = isEmittableIdentifier;
|
|
19
|
+
exports.assertEmittableSchema = assertEmittableSchema;
|
|
18
20
|
exports.generateTypes = generateTypes;
|
|
19
21
|
exports.generateZod = generateZod;
|
|
20
22
|
exports.generateMetadata = generateMetadata;
|
|
@@ -22,6 +24,7 @@ exports.generateIndex = generateIndex;
|
|
|
22
24
|
exports.generatePrismaMap = generatePrismaMap;
|
|
23
25
|
const node_fs_1 = require("node:fs");
|
|
24
26
|
const node_path_1 = require("node:path");
|
|
27
|
+
const errors_js_1 = require("./errors.js");
|
|
25
28
|
const schema_js_1 = require("./schema.js");
|
|
26
29
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
27
30
|
function entityName(tableName) {
|
|
@@ -69,9 +72,59 @@ function writeColumnTsType(col, enums) {
|
|
|
69
72
|
const widened = isArray ? '(string | Date)[]' : 'string | Date';
|
|
70
73
|
return col.nullable ? `${widened} | null` : widened;
|
|
71
74
|
}
|
|
72
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Characters that must never reach generated source verbatim: the two that end
|
|
77
|
+
* a single-quoted literal (backslash, quote), the C0/C1 control range (a raw
|
|
78
|
+
* newline alone leaves the literal unterminated and starts a new source line),
|
|
79
|
+
* and the two Unicode line terminators.
|
|
80
|
+
*/
|
|
81
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: neutralizing control characters is this pattern's purpose
|
|
82
|
+
const UNSAFE_EMIT_CHARS_GLOBAL = /[\\'\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
83
|
+
/**
|
|
84
|
+
* Escape a value for embedding in a single-quoted TypeScript string literal.
|
|
85
|
+
*
|
|
86
|
+
* Every character that can END the literal has to be neutralized here, not just
|
|
87
|
+
* the quote. A database identifier is attacker-controlled text (Postgres allows
|
|
88
|
+
* any character in a double-quoted name, up to 63 bytes) and generated code is
|
|
89
|
+
* `import`ed, i.e. EXECUTED, so a literal that closes early leaves the rest of
|
|
90
|
+
* the name in expression position. A raw newline is enough on its own.
|
|
91
|
+
*/
|
|
73
92
|
function escSQ(value) {
|
|
74
|
-
return value.replace(
|
|
93
|
+
return value.replace(UNSAFE_EMIT_CHARS_GLOBAL, (ch) => {
|
|
94
|
+
switch (ch) {
|
|
95
|
+
case '\\':
|
|
96
|
+
return '\\\\';
|
|
97
|
+
case "'":
|
|
98
|
+
return "\\'";
|
|
99
|
+
case '\n':
|
|
100
|
+
return '\\n';
|
|
101
|
+
case '\r':
|
|
102
|
+
return '\\r';
|
|
103
|
+
case '\t':
|
|
104
|
+
return '\\t';
|
|
105
|
+
case '\b':
|
|
106
|
+
return '\\b';
|
|
107
|
+
case '\f':
|
|
108
|
+
return '\\f';
|
|
109
|
+
case '\v':
|
|
110
|
+
return '\\v';
|
|
111
|
+
default:
|
|
112
|
+
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: collapsing control characters is this pattern's purpose
|
|
117
|
+
const COMMENT_BREAKING_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g;
|
|
118
|
+
/**
|
|
119
|
+
* Neutralize a catalog string for emission inside a generated JSDoc or line
|
|
120
|
+
* COMMENT. Two sequences end a comment early and both are legal in a
|
|
121
|
+
* double-quoted Postgres identifier: an asterisk followed by a slash closes a
|
|
122
|
+
* block comment, putting everything after it in code position, and a newline
|
|
123
|
+
* ends a line comment and splits one emitted line into two. Escaping the
|
|
124
|
+
* asterisk breaks the terminator without changing how the name reads.
|
|
125
|
+
*/
|
|
126
|
+
function docSafe(value) {
|
|
127
|
+
return value.replace(/\*\//g, '*\\/').replace(COMMENT_BREAKING_CHARS, ' ');
|
|
75
128
|
}
|
|
76
129
|
// ---------------------------------------------------------------------------
|
|
77
130
|
// Main generate function
|
|
@@ -296,12 +349,75 @@ function typeSafeRelations(table, warn = true) {
|
|
|
296
349
|
}
|
|
297
350
|
return usable;
|
|
298
351
|
}
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Identifier boundary
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
/**
|
|
356
|
+
* The JavaScript identifier grammar as a WHOLE-STRING match: `IdentifierStart`
|
|
357
|
+
* (`ID_Start`, `$`, `_`) followed by `IdentifierPart` (`ID_Continue`, `$`, ZWNJ,
|
|
358
|
+
* ZWJ). Deliberately Unicode-aware rather than `[A-Za-z_$][\w$]*`: a table named
|
|
359
|
+
* `café` yields the perfectly valid identifier `Café` and there is no reason to
|
|
360
|
+
* refuse it. What it does refuse is every character that could end the
|
|
361
|
+
* identifier token, so a name that passes interpolates as exactly one token.
|
|
362
|
+
*/
|
|
363
|
+
const EMITTABLE_IDENTIFIER_RE = /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u;
|
|
364
|
+
/**
|
|
365
|
+
* Whether `name` can be interpolated into generated TypeScript in IDENTIFIER
|
|
366
|
+
* position (a type name, an interface name, a class member declaration).
|
|
367
|
+
*
|
|
368
|
+
* Identifier position is the one emission site with no escaping option: a value
|
|
369
|
+
* position becomes a quoted literal ({@link escSQ}), a key position becomes a
|
|
370
|
+
* quoted key ({@link quoteIfNeeded}), a comment is neutralized
|
|
371
|
+
* ({@link docSafe}), but `export interface X` needs a real identifier. So the
|
|
372
|
+
* only sound answer for a name that is not one is to refuse.
|
|
373
|
+
*/
|
|
374
|
+
function isEmittableIdentifier(name) {
|
|
375
|
+
return EMITTABLE_IDENTIFIER_RE.test(name);
|
|
376
|
+
}
|
|
377
|
+
function requireEmittable(derived, subject, role) {
|
|
378
|
+
if (isEmittableIdentifier(derived))
|
|
379
|
+
return;
|
|
380
|
+
throw new errors_js_1.ValidationError(`[turbine] Cannot generate code for ${subject}: it produces ${JSON.stringify(derived)} as ${role}, ` +
|
|
381
|
+
`which is not a valid TypeScript identifier. Rename the database object, or exclude it from generation ` +
|
|
382
|
+
`(introspect \`exclude\`).`);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Refuse a schema whose names cannot be emitted as TypeScript identifiers.
|
|
386
|
+
*
|
|
387
|
+
* Called by every emitter that puts a catalog-derived name in identifier
|
|
388
|
+
* position ({@link generateTypes}, {@link generateIndex}, {@link generateZod}).
|
|
389
|
+
* {@link generateMetadata} deliberately does NOT call it: metadata.ts emits no
|
|
390
|
+
* identifiers derived from catalog names, every name there is a quoted key or a
|
|
391
|
+
* quoted value, so it has no identifier rule to enforce and stays usable for a
|
|
392
|
+
* schema whose type layer cannot be generated.
|
|
393
|
+
*
|
|
394
|
+
* The check runs on the DERIVED identifier, not the raw name, because that is
|
|
395
|
+
* what actually lands in the output, but the message names the raw object so
|
|
396
|
+
* the reader knows what to rename.
|
|
397
|
+
*/
|
|
398
|
+
function assertEmittableSchema(schema) {
|
|
399
|
+
for (const enumName of Object.keys(schema.enums)) {
|
|
400
|
+
requireEmittable((0, schema_js_1.snakeToPascal)(enumName), `enum type "${enumName}"`, 'the generated enum type name');
|
|
401
|
+
}
|
|
402
|
+
for (const table of Object.values(schema.tables)) {
|
|
403
|
+
requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
|
|
404
|
+
requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
|
|
405
|
+
// Only the relations that reach the TYPE layer: a relation shadowing a
|
|
406
|
+
// column field is already dropped from types.ts, so refusing on its name
|
|
407
|
+
// would refuse a schema that generates fine.
|
|
408
|
+
for (const [relName, rel] of typeSafeRelations(table, false)) {
|
|
409
|
+
requireEmittable((0, schema_js_1.snakeToPascal)(relName), `relation "${relName}" on table "${table.name}"`, 'part of the generated `XWithY` interface name');
|
|
410
|
+
requireEmittable(entityName(rel.to), `relation "${relName}" on table "${table.name}" (target "${rel.to}")`, 'the generated target entity type name');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
299
414
|
/**
|
|
300
415
|
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
301
416
|
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
302
417
|
* generator output without writing files to disk.
|
|
303
418
|
*/
|
|
304
419
|
function generateTypes(schema, options) {
|
|
420
|
+
assertEmittableSchema(schema);
|
|
305
421
|
const lines = [...generatedFileHeader(options)];
|
|
306
422
|
// We import UpdateOperatorInput so generated *Update types can express
|
|
307
423
|
// atomic increment / decrement / multiply / divide / set operators on
|
|
@@ -332,7 +448,7 @@ function generateTypes(schema, options) {
|
|
|
332
448
|
// Generate enum types
|
|
333
449
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
334
450
|
const typeName = (0, schema_js_1.snakeToPascal)(enumName);
|
|
335
|
-
lines.push(`/** Database enum: ${enumName} */`);
|
|
451
|
+
lines.push(`/** Database enum: ${docSafe(enumName)} */`);
|
|
336
452
|
lines.push(`export type ${typeName} = ${labels.map((l) => `'${escSQ(l)}'`).join(' | ')};`);
|
|
337
453
|
lines.push('');
|
|
338
454
|
}
|
|
@@ -340,7 +456,7 @@ function generateTypes(schema, options) {
|
|
|
340
456
|
for (const table of Object.values(schema.tables)) {
|
|
341
457
|
const typeName = entityName(table.name);
|
|
342
458
|
// --- Base entity interface ---
|
|
343
|
-
lines.push(`/** Row type for the \`${table.name}\` table */`);
|
|
459
|
+
lines.push(`/** Row type for the \`${docSafe(table.name)}\` table */`);
|
|
344
460
|
lines.push(`export interface ${typeName} {`);
|
|
345
461
|
for (const col of table.columns) {
|
|
346
462
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
@@ -350,7 +466,7 @@ function generateTypes(schema, options) {
|
|
|
350
466
|
// The emitted type marks it optional so it tells the truth about absence.
|
|
351
467
|
const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
|
|
352
468
|
const optional = col.pii ? '?' : '';
|
|
353
|
-
lines.push(` /** Column: ${col.name}, ${col.pgType}${pkNote}${nullNote}${piiNote} */`);
|
|
469
|
+
lines.push(` /** Column: ${docSafe(col.name)}, ${docSafe(col.pgType)}${pkNote}${nullNote}${piiNote} */`);
|
|
354
470
|
lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${columnTsType(col, schema.enums)};`);
|
|
355
471
|
}
|
|
356
472
|
lines.push('}');
|
|
@@ -358,7 +474,7 @@ function generateTypes(schema, options) {
|
|
|
358
474
|
// --- Create input type ---
|
|
359
475
|
// Required: non-nullable columns without defaults (except PK)
|
|
360
476
|
// Optional: nullable columns (default to NULL) or columns with explicit defaults
|
|
361
|
-
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
477
|
+
lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
|
|
362
478
|
lines.push(`export type ${typeName}Create = {`);
|
|
363
479
|
for (const col of table.columns) {
|
|
364
480
|
// STORED generated columns are computed by the database, never writable.
|
|
@@ -381,7 +497,7 @@ function generateTypes(schema, options) {
|
|
|
381
497
|
// Numeric columns additionally accept `UpdateOperatorInput<number>` so
|
|
382
498
|
// users can write `{ viewCount: { increment: 1 } }` without an `as any`.
|
|
383
499
|
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
|
|
384
|
-
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
500
|
+
lines.push(`/** Input type for updating a row in \`${docSafe(table.name)}\` */`);
|
|
385
501
|
lines.push(`export type ${typeName}Update = {`);
|
|
386
502
|
for (const col of nonPkCols) {
|
|
387
503
|
lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(writeColumnTsType(col, schema.enums))};`);
|
|
@@ -399,14 +515,14 @@ function generateTypes(schema, options) {
|
|
|
399
515
|
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
400
516
|
const hasRelations = safeRelations.length > 0;
|
|
401
517
|
if (hasRelations) {
|
|
402
|
-
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
518
|
+
lines.push(`/** Available relations for the \`${docSafe(table.name)}\` table */`);
|
|
403
519
|
lines.push(`export interface ${typeName}Relations {`);
|
|
404
520
|
for (const [relName, rel] of safeRelations) {
|
|
405
521
|
const targetType = entityName(rel.to);
|
|
406
522
|
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
407
523
|
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
408
524
|
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
409
|
-
lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
525
|
+
lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
410
526
|
}
|
|
411
527
|
lines.push('}');
|
|
412
528
|
lines.push('');
|
|
@@ -414,15 +530,15 @@ function generateTypes(schema, options) {
|
|
|
414
530
|
for (const [relName, rel] of safeRelations) {
|
|
415
531
|
const targetType = entityName(rel.to);
|
|
416
532
|
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
417
|
-
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
533
|
+
lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
|
|
418
534
|
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
419
|
-
lines.push(` ${relName}: ${targetType}[];`);
|
|
535
|
+
lines.push(` ${quoteIfNeeded(relName)}: ${targetType}[];`);
|
|
420
536
|
lines.push('}');
|
|
421
537
|
}
|
|
422
538
|
else {
|
|
423
|
-
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
539
|
+
lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
|
|
424
540
|
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
425
|
-
lines.push(` ${relName}: ${targetType} | null;`);
|
|
541
|
+
lines.push(` ${quoteIfNeeded(relName)}: ${targetType} | null;`);
|
|
426
542
|
lines.push('}');
|
|
427
543
|
}
|
|
428
544
|
lines.push('');
|
|
@@ -519,7 +635,7 @@ function generateTypes(schema, options) {
|
|
|
519
635
|
lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
|
|
520
636
|
for (const [relName, rel] of safeRelations) {
|
|
521
637
|
const targetType = entityName(rel.to);
|
|
522
|
-
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
638
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
|
|
523
639
|
}
|
|
524
640
|
lines.push('};');
|
|
525
641
|
lines.push('');
|
|
@@ -527,10 +643,10 @@ function generateTypes(schema, options) {
|
|
|
527
643
|
for (const [relName, rel] of safeRelations) {
|
|
528
644
|
const targetType = entityName(rel.to);
|
|
529
645
|
if (rel.type === 'hasMany') {
|
|
530
|
-
lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
|
|
646
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedUpdateInput;`);
|
|
531
647
|
}
|
|
532
648
|
else {
|
|
533
|
-
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
649
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
|
|
534
650
|
}
|
|
535
651
|
}
|
|
536
652
|
lines.push('};');
|
|
@@ -635,6 +751,7 @@ function zodBaseType(col, enums, forWrite = false) {
|
|
|
635
751
|
* the output without writing files.
|
|
636
752
|
*/
|
|
637
753
|
function generateZod(schema, options) {
|
|
754
|
+
assertEmittableSchema(schema);
|
|
638
755
|
const lines = [...generatedFileHeader(options)];
|
|
639
756
|
// `zod` is a USER dependency, this generated file imports it, but the Turbine
|
|
640
757
|
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
@@ -643,7 +760,7 @@ function generateZod(schema, options) {
|
|
|
643
760
|
for (const table of Object.values(schema.tables)) {
|
|
644
761
|
const typeName = entityName(table.name);
|
|
645
762
|
// Full-row schema.
|
|
646
|
-
lines.push(`/** Zod schema for a \`${table.name}\` row */`);
|
|
763
|
+
lines.push(`/** Zod schema for a \`${docSafe(table.name)}\` row */`);
|
|
647
764
|
lines.push(`export const ${typeName}Schema = z.object({`);
|
|
648
765
|
for (const col of table.columns) {
|
|
649
766
|
let expr = zodBaseType(col, schema.enums);
|
|
@@ -655,7 +772,7 @@ function generateZod(schema, options) {
|
|
|
655
772
|
lines.push('');
|
|
656
773
|
// Create schema, STORED generated columns can never be written; PK,
|
|
657
774
|
// defaulted, and nullable columns are optional.
|
|
658
|
-
lines.push(`/** Zod schema for creating a \`${table.name}\` row */`);
|
|
775
|
+
lines.push(`/** Zod schema for creating a \`${docSafe(table.name)}\` row */`);
|
|
659
776
|
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
660
777
|
for (const col of table.columns) {
|
|
661
778
|
if (col.isGeneratedStored)
|
|
@@ -671,7 +788,7 @@ function generateZod(schema, options) {
|
|
|
671
788
|
lines.push('});');
|
|
672
789
|
lines.push('');
|
|
673
790
|
// Update schema, PK and STORED generated columns omitted; all else optional.
|
|
674
|
-
lines.push(`/** Zod schema for updating a \`${table.name}\` row */`);
|
|
791
|
+
lines.push(`/** Zod schema for updating a \`${docSafe(table.name)}\` row */`);
|
|
675
792
|
lines.push(`export const ${typeName}UpdateSchema = z.object({`);
|
|
676
793
|
for (const col of table.columns) {
|
|
677
794
|
if (col.isGeneratedStored)
|
|
@@ -701,7 +818,7 @@ function generateMetadata(schema, options) {
|
|
|
701
818
|
' tables: {',
|
|
702
819
|
];
|
|
703
820
|
for (const table of Object.values(schema.tables)) {
|
|
704
|
-
lines.push(` ${table.name}: {`);
|
|
821
|
+
lines.push(` ${quoteIfNeeded(table.name)}: {`);
|
|
705
822
|
lines.push(` name: '${escSQ(table.name)}',`);
|
|
706
823
|
// columns
|
|
707
824
|
lines.push(' columns: [');
|
|
@@ -762,7 +879,7 @@ function generateMetadata(schema, options) {
|
|
|
762
879
|
`sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
|
|
763
880
|
`targetKey: ${keyLiteral(rel.through.targetKey)} }`;
|
|
764
881
|
}
|
|
765
|
-
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
882
|
+
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} },`);
|
|
766
883
|
}
|
|
767
884
|
lines.push(' },');
|
|
768
885
|
// indexes
|
|
@@ -795,7 +912,7 @@ function generateMetadata(schema, options) {
|
|
|
795
912
|
// enums
|
|
796
913
|
lines.push(' enums: {');
|
|
797
914
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
798
|
-
lines.push(` ${enumName}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
915
|
+
lines.push(` ${quoteIfNeeded(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
799
916
|
}
|
|
800
917
|
lines.push(' },');
|
|
801
918
|
lines.push('};');
|
|
@@ -810,6 +927,7 @@ function generateMetadata(schema, options) {
|
|
|
810
927
|
// index.ts generator (configured client with typed table accessors)
|
|
811
928
|
// ---------------------------------------------------------------------------
|
|
812
929
|
function generateIndex(schema, options) {
|
|
930
|
+
assertEmittableSchema(schema);
|
|
813
931
|
const tableEntries = Object.values(schema.tables);
|
|
814
932
|
// Must mirror generateTypes: `XRelations` only exists in types.ts when the
|
|
815
933
|
// table has at least one type-safe (non-column-shadowing) relation.
|
|
@@ -856,7 +974,7 @@ function generateIndex(schema, options) {
|
|
|
856
974
|
const accessor = snakeToCamelStr(table.name);
|
|
857
975
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
858
976
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
859
|
-
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
977
|
+
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table (transaction-scoped) */`);
|
|
860
978
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
861
979
|
}
|
|
862
980
|
lines.push('}');
|
|
@@ -879,7 +997,7 @@ function generateIndex(schema, options) {
|
|
|
879
997
|
lines.push(' *');
|
|
880
998
|
lines.push(' * Tables:');
|
|
881
999
|
for (const table of tableEntries) {
|
|
882
|
-
lines.push(` * - \`${snakeToCamelStr(table.name)}\` (${table.name})`);
|
|
1000
|
+
lines.push(` * - \`${docSafe(snakeToCamelStr(table.name))}\` (${docSafe(table.name)})`);
|
|
883
1001
|
}
|
|
884
1002
|
lines.push(' *');
|
|
885
1003
|
lines.push(' * @example');
|
|
@@ -888,7 +1006,7 @@ function generateIndex(schema, options) {
|
|
|
888
1006
|
if (tableEntries.length > 0) {
|
|
889
1007
|
const firstTable = tableEntries[0];
|
|
890
1008
|
const accessor = snakeToCamelStr(firstTable.name);
|
|
891
|
-
lines.push(` * const rows = await db.${accessor}.findMany();`);
|
|
1009
|
+
lines.push(` * const rows = await db.${docSafe(accessor)}.findMany();`);
|
|
892
1010
|
}
|
|
893
1011
|
lines.push(' * ```');
|
|
894
1012
|
lines.push(' */');
|
|
@@ -898,7 +1016,7 @@ function generateIndex(schema, options) {
|
|
|
898
1016
|
const accessor = snakeToCamelStr(table.name);
|
|
899
1017
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
900
1018
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
901
|
-
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
1019
|
+
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table */`);
|
|
902
1020
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
903
1021
|
}
|
|
904
1022
|
lines.push('');
|
|
@@ -1073,8 +1191,37 @@ function serializeColumn(col) {
|
|
|
1073
1191
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
1074
1192
|
return `{ ${parts.join(', ')} }`;
|
|
1075
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* The subset of names emitted BARE in generated object-key position. Anchored
|
|
1196
|
+
* on purpose: the previous rule only tested whether a name contained a
|
|
1197
|
+
* character outside the set, so a name made entirely of allowed characters but
|
|
1198
|
+
* starting with a digit (`2fa`) was emitted bare and did not parse.
|
|
1199
|
+
*/
|
|
1200
|
+
const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1201
|
+
/**
|
|
1202
|
+
* Emit `s` in generated object-KEY position.
|
|
1203
|
+
*
|
|
1204
|
+
* A key is the one place in the output where catalog text would otherwise land
|
|
1205
|
+
* in EXPRESSION position: `{ [expr]: v }` is a COMPUTED key, evaluated when the
|
|
1206
|
+
* object is constructed, so an unquoted name carrying brackets runs on
|
|
1207
|
+
* `import`. The bare form is therefore gated on a WHOLE-STRING match of the
|
|
1208
|
+
* identifier grammar, which no bracket, parenthesis, quote, newline, or space
|
|
1209
|
+
* can pass; every other name becomes a fully escaped string literal, which is
|
|
1210
|
+
* inert in key position.
|
|
1211
|
+
*
|
|
1212
|
+
* The literal has to be ESCAPED, not merely wrapped: this function used to
|
|
1213
|
+
* return `'${s}'` verbatim, so a name containing a quote closed the key and
|
|
1214
|
+
* reopened in expression position.
|
|
1215
|
+
*
|
|
1216
|
+
* KNOWN LIMIT, deliberately not handled here: a database object named
|
|
1217
|
+
* `__proto__` still sets the emitted object literal's PROTOTYPE rather than
|
|
1218
|
+
* adding a property, because the object-literal special case applies to the
|
|
1219
|
+
* quoted spelling too. Quoting is not a fix for it, so there is no branch for
|
|
1220
|
+
* it; the consequence is a metadata map that silently omits that one entry, not
|
|
1221
|
+
* code execution.
|
|
1222
|
+
*/
|
|
1076
1223
|
function quoteIfNeeded(s) {
|
|
1077
|
-
return
|
|
1224
|
+
return BARE_KEY_RE.test(s) ? s : `'${escSQ(s)}'`;
|
|
1078
1225
|
}
|
|
1079
1226
|
function snakeToCamelStr(s) {
|
|
1080
1227
|
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
package/dist/cjs/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/cjs/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* // npx turbine generate
|
|
12
12
|
*
|
|
13
13
|
* // 2. Import the generated client:
|
|
14
|
-
* import { turbine } from './generated/turbine';
|
|
14
|
+
* import { turbine } from './generated/turbine/index.js';
|
|
15
15
|
*
|
|
16
16
|
* const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
17
17
|
*
|
package/dist/cjs/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/cjs/introspect.js
CHANGED
|
@@ -16,6 +16,8 @@ exports.DEFAULT_EXCLUDED_TABLES = void 0;
|
|
|
16
16
|
exports.pgConfActionToReferential = pgConfActionToReferential;
|
|
17
17
|
exports.applyTableFilters = applyTableFilters;
|
|
18
18
|
exports.defaultExcludedTablesPresent = defaultExcludedTablesPresent;
|
|
19
|
+
exports.assertSafeCatalogIdentifier = assertSafeCatalogIdentifier;
|
|
20
|
+
exports.assertSafeCatalogSchema = assertSafeCatalogSchema;
|
|
19
21
|
exports.introspect = introspect;
|
|
20
22
|
exports.applyRelationRenames = applyRelationRenames;
|
|
21
23
|
exports.introspectPostgresCatalog = introspectPostgresCatalog;
|
|
@@ -326,6 +328,99 @@ function defaultExcludedTablesPresent(names, options = {}) {
|
|
|
326
328
|
// ---------------------------------------------------------------------------
|
|
327
329
|
// Main introspection function
|
|
328
330
|
// ---------------------------------------------------------------------------
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Catalog identifier boundary
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
/**
|
|
335
|
+
* Characters that no legitimate SQL object name carries and that are exactly
|
|
336
|
+
* the primitives for breaking OUT of a generated string literal or comment: the
|
|
337
|
+
* C0 control range (NUL, newline, carriage return, tab), the C1 range, and the
|
|
338
|
+
* two Unicode line terminators.
|
|
339
|
+
*
|
|
340
|
+
* Postgres permits ANY character in a double-quoted identifier up to 63 bytes,
|
|
341
|
+
* so a catalog name is attacker-controlled text as soon as anyone but the DBA
|
|
342
|
+
* can create an object. `turbine generate` turns those names into TypeScript
|
|
343
|
+
* that is then `import`ed, i.e. EXECUTED, and `turbine studio` / the MCP server
|
|
344
|
+
* render them into HTML and JSON. Escaping at each of those sinks is the actual
|
|
345
|
+
* fix (see `escSQ` / `quoteIfNeeded` / `docSafe` in generate.ts); this boundary
|
|
346
|
+
* is the belt-and-braces refusal one layer earlier, and it names the object so
|
|
347
|
+
* the operator can see WHICH one is malformed instead of debugging generated
|
|
348
|
+
* output.
|
|
349
|
+
*
|
|
350
|
+
* Deliberately narrow. It does NOT refuse a name that merely cannot become a
|
|
351
|
+
* TypeScript identifier (`2fa_codes`), because `introspect()` also feeds
|
|
352
|
+
* Studio, the MCP server, and `doctor`, none of which emit identifiers, and
|
|
353
|
+
* refusing there would break tools that work today. That question belongs to
|
|
354
|
+
* the code generator and is answered by `assertEmittableSchema` in generate.ts.
|
|
355
|
+
*/
|
|
356
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this pattern's purpose
|
|
357
|
+
const UNSAFE_CATALOG_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
358
|
+
/**
|
|
359
|
+
* Refuse one catalog identifier carrying a character from
|
|
360
|
+
* {@link UNSAFE_CATALOG_CHARS}. `subject` names the object in the error.
|
|
361
|
+
*/
|
|
362
|
+
function assertSafeCatalogIdentifier(name, subject) {
|
|
363
|
+
const match = UNSAFE_CATALOG_CHARS.exec(name);
|
|
364
|
+
if (match === null)
|
|
365
|
+
return;
|
|
366
|
+
const code = match[0].charCodeAt(0).toString(16).padStart(4, '0');
|
|
367
|
+
throw new errors_js_1.ValidationError(`[turbine] Refusing to introspect ${subject}: its name contains the control character U+${code.toUpperCase()} ` +
|
|
368
|
+
`at position ${match.index}. Such a name cannot be safely emitted into generated code, SQL comments, or ` +
|
|
369
|
+
`tooling output. Rename the database object, or exclude it from introspection.`);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Walk a freshly introspected {@link SchemaMetadata} and refuse any catalog
|
|
373
|
+
* identifier that carries a control character (see
|
|
374
|
+
* {@link assertSafeCatalogIdentifier}).
|
|
375
|
+
*
|
|
376
|
+
* Covers every string that is a NAME: tables, columns (catalog name and derived
|
|
377
|
+
* field), relations (key, name, endpoints, keys, junction), enum types and their
|
|
378
|
+
* labels, index names and their columns, and check-constraint names. It does
|
|
379
|
+
* NOT cover free-text SQL, index definitions, check expressions, and column
|
|
380
|
+
* defaults are emitted with `JSON.stringify` and are not identifiers.
|
|
381
|
+
*/
|
|
382
|
+
function assertSafeCatalogSchema(schema) {
|
|
383
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
384
|
+
assertSafeCatalogIdentifier(enumName, `enum type "${enumName}"`);
|
|
385
|
+
for (const label of labels) {
|
|
386
|
+
assertSafeCatalogIdentifier(label, `a label of enum type "${enumName}"`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const [tableKey, table] of Object.entries(schema.tables)) {
|
|
390
|
+
assertSafeCatalogIdentifier(tableKey, `table "${tableKey}"`);
|
|
391
|
+
assertSafeCatalogIdentifier(table.name, `table "${tableKey}"`);
|
|
392
|
+
const where = `table "${table.name}"`;
|
|
393
|
+
for (const col of table.columns) {
|
|
394
|
+
assertSafeCatalogIdentifier(col.name, `column "${col.name}" on ${where}`);
|
|
395
|
+
assertSafeCatalogIdentifier(col.field, `the field name derived for a column on ${where}`);
|
|
396
|
+
}
|
|
397
|
+
for (const [relKey, rel] of Object.entries(table.relations)) {
|
|
398
|
+
const relWhere = `relation "${relKey}" on ${where}`;
|
|
399
|
+
assertSafeCatalogIdentifier(relKey, relWhere);
|
|
400
|
+
assertSafeCatalogIdentifier(rel.name, relWhere);
|
|
401
|
+
assertSafeCatalogIdentifier(rel.from, `the source table of ${relWhere}`);
|
|
402
|
+
assertSafeCatalogIdentifier(rel.to, `the target table of ${relWhere}`);
|
|
403
|
+
for (const k of [rel.foreignKey, rel.referenceKey].flat()) {
|
|
404
|
+
assertSafeCatalogIdentifier(k, `a key column of ${relWhere}`);
|
|
405
|
+
}
|
|
406
|
+
if (rel.through) {
|
|
407
|
+
assertSafeCatalogIdentifier(rel.through.table, `the junction table of ${relWhere}`);
|
|
408
|
+
for (const k of [rel.through.sourceKey, rel.through.targetKey].flat()) {
|
|
409
|
+
assertSafeCatalogIdentifier(k, `a junction key column of ${relWhere}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const idx of table.indexes) {
|
|
414
|
+
assertSafeCatalogIdentifier(idx.name, `index "${idx.name}" on ${where}`);
|
|
415
|
+
for (const c of idx.columns) {
|
|
416
|
+
assertSafeCatalogIdentifier(c, `a column of index "${idx.name}" on ${where}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const chk of table.checks ?? []) {
|
|
420
|
+
assertSafeCatalogIdentifier(chk.name, `check constraint "${chk.name}" on ${where}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
329
424
|
/**
|
|
330
425
|
* Introspect a database into {@link SchemaMetadata}, routing through the active
|
|
331
426
|
* dialect's {@link Dialect.introspector} so each engine can override the catalog
|
|
@@ -339,7 +434,11 @@ async function introspect(options) {
|
|
|
339
434
|
: // Dialects without an introspector fall back to the Postgres catalog reader.
|
|
340
435
|
await introspectPostgresCatalog(options);
|
|
341
436
|
// Applied here rather than inside each introspector so every engine gets it.
|
|
342
|
-
|
|
437
|
+
const renamed = options.relationNames ? applyRelationRenames(schema, options.relationNames) : schema;
|
|
438
|
+
// Same reason: one boundary for every engine, and AFTER the renames so a
|
|
439
|
+
// caller-supplied relation name is checked too.
|
|
440
|
+
assertSafeCatalogSchema(renamed);
|
|
441
|
+
return renamed;
|
|
343
442
|
}
|
|
344
443
|
/**
|
|
345
444
|
* Rename introspected relations, per table, from the name turbine derived to
|
package/dist/cjs/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/cjs/mssql.js
CHANGED
|
@@ -104,6 +104,7 @@ const dialect_js_1 = require("./dialect.js");
|
|
|
104
104
|
const errors_js_1 = require("./errors.js");
|
|
105
105
|
const introspect_js_1 = require("./introspect.js");
|
|
106
106
|
const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
|
|
107
|
+
const utils_js_1 = require("./query/utils.js");
|
|
107
108
|
const schema_js_1 = require("./schema.js");
|
|
108
109
|
// ---------------------------------------------------------------------------
|
|
109
110
|
// SQL Server / connection limits
|
|
@@ -368,6 +369,24 @@ class MssqlTxClient {
|
|
|
368
369
|
class MssqlPool {
|
|
369
370
|
/** The underlying `mssql` ConnectionPool, exposed as an escape hatch (seed / DDL / advanced ops). */
|
|
370
371
|
pool;
|
|
372
|
+
/**
|
|
373
|
+
* The dialect this pool speaks, published so a consumer holding only the POOL
|
|
374
|
+
* can find it.
|
|
375
|
+
*
|
|
376
|
+
* `executePipeline` (src/pipeline.ts) is that consumer: it receives a pool and
|
|
377
|
+
* nothing else, and its transaction control used to be the literal strings
|
|
378
|
+
* `BEGIN` / `COMMIT` / `ROLLBACK`. A bare `BEGIN` is a statement-BLOCK opener
|
|
379
|
+
* in T-SQL, so it missed {@link MssqlTxClient}'s `BEGIN TRAN(SACTION)` branch,
|
|
380
|
+
* reached the server as a block with no `END`, and was rejected; the COMMIT
|
|
381
|
+
* and ROLLBACK behind it then found no open transaction and no-oped. Reading
|
|
382
|
+
* the dialect off the pool is what lets the batch emit `BEGIN TRANSACTION`
|
|
383
|
+
* and land on the driver's Transaction API instead.
|
|
384
|
+
*
|
|
385
|
+
* SQL Server is the only engine whose transaction keywords differ from
|
|
386
|
+
* Postgres's, so it is the only pool shim that needs to publish this; the
|
|
387
|
+
* lookup treats an absent `dialect` as PostgreSQL.
|
|
388
|
+
*/
|
|
389
|
+
dialect = exports.mssqlDialect;
|
|
371
390
|
sqlNS;
|
|
372
391
|
closed = false;
|
|
373
392
|
constructor(pool, sqlNS) {
|
|
@@ -897,7 +916,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
897
916
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
898
917
|
if (!nestedRelDef) {
|
|
899
918
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
900
|
-
|
|
919
|
+
(0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), 'It has no relations.'));
|
|
901
920
|
}
|
|
902
921
|
const sub = ctx.recurse(nestedRelDef, nestedSpec, parentAlias, depth + 1, [...path, relDef.name]);
|
|
903
922
|
cols.push(`JSON_QUERY((${sub})) AS ${q(nestedRelName)}`);
|