turbine-orm 0.76.0 → 0.77.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/dist/cjs/cli/index.js +2 -2
- package/dist/cjs/cli/migrate.js +8 -8
- package/dist/cjs/cli/studio.js +6 -3
- package/dist/cjs/client.js +10 -10
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +21 -25
- package/dist/cjs/generate.js +1 -1
- package/dist/cjs/introspect.js +5 -5
- package/dist/cjs/mssql.js +12 -12
- package/dist/cjs/mysql.js +5 -5
- package/dist/cjs/nested-write.js +31 -31
- package/dist/cjs/observe.js +1 -1
- package/dist/cjs/pipeline-submittable.js +5 -1
- package/dist/cjs/pipeline.js +8 -0
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +4 -4
- package/dist/cjs/powdb-shared.d.ts +348 -0
- package/dist/cjs/powdb-shared.js +587 -0
- package/dist/cjs/powdb.d.ts +12 -299
- package/dist/cjs/powdb.js +106 -567
- package/dist/cjs/powql.d.ts +1 -1
- package/dist/cjs/powql.js +89 -89
- package/dist/cjs/prisma-compat.js +16 -16
- package/dist/cjs/query/aggregates.js +19 -20
- package/dist/cjs/query/batched-loader.js +8 -8
- package/dist/cjs/query/builder.js +8 -8
- package/dist/cjs/query/compound-unique.js +2 -2
- package/dist/cjs/query/filters.js +1 -1
- package/dist/cjs/query/relations.js +60 -48
- package/dist/cjs/query/types.js +4 -4
- package/dist/cjs/query/utils.js +5 -5
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/where-compile.js +1 -1
- package/dist/cjs/query/where.js +30 -31
- package/dist/cjs/query/writes.js +7 -7
- package/dist/cjs/realtime.js +4 -4
- package/dist/cjs/schema-metadata.js +1 -1
- package/dist/cjs/schema-sql.js +3 -3
- package/dist/cjs/seed.js +1 -1
- package/dist/cjs/sqlite.js +7 -7
- package/dist/cjs/typed-sql.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/migrate.js +8 -8
- package/dist/cli/studio.js +6 -3
- package/dist/client.js +10 -10
- package/dist/dialect.js +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +21 -25
- package/dist/generate.js +1 -1
- package/dist/introspect.js +5 -5
- package/dist/mssql.js +12 -12
- package/dist/mysql.js +5 -5
- package/dist/nested-write.js +31 -31
- package/dist/observe.js +1 -1
- package/dist/pipeline-submittable.js +6 -2
- package/dist/pipeline.js +9 -1
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +2 -2
- package/dist/powdb-shared.d.ts +348 -0
- package/dist/powdb-shared.js +570 -0
- package/dist/powdb.d.ts +12 -299
- package/dist/powdb.js +71 -534
- package/dist/powql.d.ts +1 -1
- package/dist/powql.js +43 -43
- package/dist/prisma-compat.js +16 -16
- package/dist/query/aggregates.js +19 -20
- package/dist/query/batched-loader.js +8 -8
- package/dist/query/builder.js +8 -8
- package/dist/query/compound-unique.js +2 -2
- package/dist/query/filters.js +1 -1
- package/dist/query/relations.js +60 -48
- package/dist/query/types.js +4 -4
- package/dist/query/utils.js +5 -5
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.js +30 -31
- package/dist/query/writes.js +7 -7
- package/dist/realtime.js +4 -4
- package/dist/schema-metadata.js +1 -1
- package/dist/schema-sql.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/sqlite.js +7 -7
- package/dist/typed-sql.js +1 -1
- package/package.json +15 -7
package/dist/client.js
CHANGED
|
@@ -288,7 +288,7 @@ function assertUsableConnectionString(value, source) {
|
|
|
288
288
|
// to `%20%20%20`, and resolve it against its placeholder host. Naming it as
|
|
289
289
|
// empty is more useful than reporting the phantom host.
|
|
290
290
|
if (value.trim().length === 0) {
|
|
291
|
-
throw new ConnectionError(`
|
|
291
|
+
throw new ConnectionError(`The ${source} is empty. Expected a Postgres connection string such as ` +
|
|
292
292
|
'"postgresql://user:password@host:5432/database".');
|
|
293
293
|
}
|
|
294
294
|
const verdict = classifyConnectionString(value);
|
|
@@ -299,7 +299,7 @@ function assertUsableConnectionString(value, source) {
|
|
|
299
299
|
const tail = '(Check for a missing "//", a stray quote copied out of a .env file, or a shell-truncated value.) ' +
|
|
300
300
|
'The value is not included here because it may contain a password.';
|
|
301
301
|
if (verdict.kind === 'unparseable') {
|
|
302
|
-
throw new ConnectionError(`
|
|
302
|
+
throw new ConnectionError(`The ${source} cannot be parsed as a connection string, pg rejects it with "Invalid URL" ` +
|
|
303
303
|
'(most often a non-numeric port). Expected something like ' +
|
|
304
304
|
`"postgresql://user:password@host:5432/database". ${tail}`);
|
|
305
305
|
}
|
|
@@ -310,7 +310,7 @@ function assertUsableConnectionString(value, source) {
|
|
|
310
310
|
const keywordForm = /^[A-Za-z_][A-Za-z0-9_]*\s*=/.test(value.trimStart())
|
|
311
311
|
? 'It looks like a libpq "host=… dbname=…" keyword/value string, which node-postgres does not support. '
|
|
312
312
|
: '';
|
|
313
|
-
throw new ConnectionError(`
|
|
313
|
+
throw new ConnectionError(`The ${source} names no host: it has no "postgres://" or "postgresql://" scheme, so pg would ` +
|
|
314
314
|
`resolve it as a relative URL and try to connect to its internal placeholder host. ${keywordForm}` +
|
|
315
315
|
`Expected something like "postgresql://user:password@host:5432/database". ${tail}`);
|
|
316
316
|
}
|
|
@@ -391,7 +391,7 @@ function resolveIsolationLevel(level) {
|
|
|
391
391
|
return undefined;
|
|
392
392
|
const sql = ownLookup(ISOLATION_LEVELS, level);
|
|
393
393
|
if (sql === undefined) {
|
|
394
|
-
throw new ValidationError(
|
|
394
|
+
throw new ValidationError(`$transaction: unknown isolationLevel ${JSON.stringify(level)}. ` +
|
|
395
395
|
`Expected one of: ${Object.keys(ISOLATION_LEVELS).join(', ')} (case-sensitive).`);
|
|
396
396
|
}
|
|
397
397
|
return sql;
|
|
@@ -740,7 +740,7 @@ export class TurbineClient {
|
|
|
740
740
|
// output), not runtime SchemaMetadata, so name the conversion rather than
|
|
741
741
|
// just the requirement.
|
|
742
742
|
const looksLikeSchemaDef = schema !== null && typeof schema === 'object' && Object.hasOwn(schema, 'name') === false;
|
|
743
|
-
throw new ValidationError('
|
|
743
|
+
throw new ValidationError('TurbineClient requires schema metadata as its second argument. ' +
|
|
744
744
|
'Run `npx turbine generate` and use the generated client (`turbine()` from your output dir), ' +
|
|
745
745
|
'or pass the generated `schemaMetadata` object: new TurbineClient(config, schemaMetadata).' +
|
|
746
746
|
(looksLikeSchemaDef
|
|
@@ -753,7 +753,7 @@ export class TurbineClient {
|
|
|
753
753
|
// the cause. Validate one table's shape here, where the fix is obvious.
|
|
754
754
|
for (const [name, meta] of Object.entries(schema.tables)) {
|
|
755
755
|
if (!meta || typeof meta !== 'object' || !Array.isArray(meta.columns)) {
|
|
756
|
-
throw new ValidationError(`
|
|
756
|
+
throw new ValidationError(`Table "${name}" in the schema passed to TurbineClient has no \`columns\` array, so this is ` +
|
|
757
757
|
'not runtime SchemaMetadata. A `defineSchema()` result is a SchemaDef: convert it with ' +
|
|
758
758
|
'`schemaDefToMetadata(def)`, or use the metadata emitted by `npx turbine generate`.');
|
|
759
759
|
}
|
|
@@ -1075,7 +1075,7 @@ export class TurbineClient {
|
|
|
1075
1075
|
if (value === undefined)
|
|
1076
1076
|
return undefined;
|
|
1077
1077
|
if (value !== 'null' && value !== 'preserve') {
|
|
1078
|
-
throw new ValidationError(`
|
|
1078
|
+
throw new ValidationError(`Invalid temporalInfinity: ${JSON.stringify(value)}. Expected 'preserve' (default: read a Postgres ` +
|
|
1079
1079
|
'temporal `infinity` as the JS number `Infinity` / `-Infinity`, which round-trips through a write ' +
|
|
1080
1080
|
"but breaks the declared `Date` type) or 'null' (read it as null, which serializes cleanly but " +
|
|
1081
1081
|
'makes it indistinguishable from a stored NULL, so a read-modify-write destroys the value).');
|
|
@@ -1087,7 +1087,7 @@ export class TurbineClient {
|
|
|
1087
1087
|
return undefined;
|
|
1088
1088
|
const matched = PLAN_CACHE_MODES.find((m) => m === mode);
|
|
1089
1089
|
if (matched === undefined) {
|
|
1090
|
-
throw new ValidationError(`
|
|
1090
|
+
throw new ValidationError(`Invalid planCacheMode: ${JSON.stringify(mode)}. Expected one of ${PLAN_CACHE_MODES.map((m) => `'${m}'`).join(', ')}.`);
|
|
1091
1091
|
}
|
|
1092
1092
|
if (dialect.supportsPlanCacheMode !== true) {
|
|
1093
1093
|
throw new UnsupportedFeatureError(`The planCacheMode option (plan_cache_mode = ${matched})`, dialect.name, '`plan_cache_mode` is a PostgreSQL plan-cache setting with no equivalent on this engine. Remove the option, ' +
|
|
@@ -1186,7 +1186,7 @@ export class TurbineClient {
|
|
|
1186
1186
|
const settled = TurbineClient.utcTimestampParserMode;
|
|
1187
1187
|
if (settled === undefined || settled === want)
|
|
1188
1188
|
return;
|
|
1189
|
-
throw new ValidationError(`
|
|
1189
|
+
throw new ValidationError(`utcTimestamps: ${want} conflicts with utcTimestamps: ${settled}, which an earlier TurbineClient ` +
|
|
1190
1190
|
'in this process already applied. The zone-less temporal READ parsers (pg OIDs 1114, 1082, 1115, 1182) are ' +
|
|
1191
1191
|
'process-global, so they cannot ' +
|
|
1192
1192
|
'differ per client, while the WRITE side is per client. Serving both values would give this client a ' +
|
|
@@ -1709,7 +1709,7 @@ export class TurbineClient {
|
|
|
1709
1709
|
}
|
|
1710
1710
|
for (const [name, value] of Object.entries(options.sessionContext)) {
|
|
1711
1711
|
if (!GUC_NAME_REGEX.test(name)) {
|
|
1712
|
-
throw new ValidationError(`
|
|
1712
|
+
throw new ValidationError(`Invalid session-context GUC name "${name}", must match ` +
|
|
1713
1713
|
'/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
|
|
1714
1714
|
}
|
|
1715
1715
|
const cfg = this.dialect.buildSetSessionConfig(name, String(value));
|
package/dist/dialect.js
CHANGED
|
@@ -81,7 +81,7 @@ export const postgresDialect = {
|
|
|
81
81
|
},
|
|
82
82
|
buildBulkInsertStatement(input) {
|
|
83
83
|
if (!input.columnArrayTypes || input.columnArrayTypes.length !== input.columns.length) {
|
|
84
|
-
throw new ValidationError(`
|
|
84
|
+
throw new ValidationError(`createMany bulk insert into "${input.table}": columnArrayTypes must supply one UNNEST cast ` +
|
|
85
85
|
`per column, got ${input.columnArrayTypes?.length ?? 0} for ${input.columns.length} columns ` +
|
|
86
86
|
`(${input.columns.join(', ')}). Schema metadata is missing a pgType for at least one column; ` +
|
|
87
87
|
'regenerate it with `npx turbine generate`.');
|
package/dist/errors.d.ts
CHANGED
|
@@ -152,11 +152,11 @@ export declare function describeTargetForMessage(target: unknown): string;
|
|
|
152
152
|
* When called with an options object and no explicit `message`, a Prisma-style
|
|
153
153
|
* message is built automatically. By default, only the where-clause keys are
|
|
154
154
|
* shown to avoid leaking PII into logs:
|
|
155
|
-
* `[
|
|
155
|
+
* `[TURBINE_E001] findUniqueOrThrow on "users" found no record matching where: { id }`
|
|
156
156
|
*
|
|
157
157
|
* Set `setErrorMessageMode('verbose')` (or pass `errorMessages: 'verbose'` to
|
|
158
158
|
* the TurbineClient constructor) to include the full where values:
|
|
159
|
-
* `[
|
|
159
|
+
* `[TURBINE_E001] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
|
|
160
160
|
*
|
|
161
161
|
* The full `where` object, `table`, and `operation` are always available as
|
|
162
162
|
* structured properties on the error instance regardless of mode.
|
package/dist/errors.js
CHANGED
|
@@ -452,11 +452,11 @@ function renderWhereForMessage(where, mode) {
|
|
|
452
452
|
* When called with an options object and no explicit `message`, a Prisma-style
|
|
453
453
|
* message is built automatically. By default, only the where-clause keys are
|
|
454
454
|
* shown to avoid leaking PII into logs:
|
|
455
|
-
* `[
|
|
455
|
+
* `[TURBINE_E001] findUniqueOrThrow on "users" found no record matching where: { id }`
|
|
456
456
|
*
|
|
457
457
|
* Set `setErrorMessageMode('verbose')` (or pass `errorMessages: 'verbose'` to
|
|
458
458
|
* the TurbineClient constructor) to include the full where values:
|
|
459
|
-
* `[
|
|
459
|
+
* `[TURBINE_E001] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
|
|
460
460
|
*
|
|
461
461
|
* The full `where` object, `table`, and `operation` are always available as
|
|
462
462
|
* structured properties on the error instance regardless of mode.
|
|
@@ -477,14 +477,14 @@ export class NotFoundError extends TurbineError {
|
|
|
477
477
|
if (!message) {
|
|
478
478
|
if (operation && table) {
|
|
479
479
|
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
480
|
-
message =
|
|
480
|
+
message = `${operation} on "${table}" found no record${wherePart}`;
|
|
481
481
|
}
|
|
482
482
|
else if (table) {
|
|
483
483
|
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
|
|
484
|
-
message = `
|
|
484
|
+
message = `No record found in "${table}"${wherePart}`;
|
|
485
485
|
}
|
|
486
486
|
else {
|
|
487
|
-
message = '
|
|
487
|
+
message = 'Record not found';
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
super(TurbineErrorCode.NOT_FOUND, message, { cause });
|
|
@@ -506,7 +506,7 @@ export class TimeoutError extends TurbineError {
|
|
|
506
506
|
* when wrapping a driver error rather than a client-side timer expiry.
|
|
507
507
|
*/
|
|
508
508
|
constructor(timeoutMs, context = 'Query', options) {
|
|
509
|
-
super(TurbineErrorCode.TIMEOUT, options?.message ??
|
|
509
|
+
super(TurbineErrorCode.TIMEOUT, options?.message ?? `${context} timed out after ${timeoutMs}ms`, options);
|
|
510
510
|
this.name = 'TimeoutError';
|
|
511
511
|
this.timeoutMs = timeoutMs;
|
|
512
512
|
}
|
|
@@ -576,7 +576,7 @@ export class ConnectionError extends TurbineError {
|
|
|
576
576
|
* placeholder password. Never derived from the caller's value.
|
|
577
577
|
*/
|
|
578
578
|
export function malformedConnectionStringMessage(engine, example) {
|
|
579
|
-
return (`
|
|
579
|
+
return (`The ${engine} connection string could not be parsed as a URL. Expected something like "${example}". ` +
|
|
580
580
|
'(Check for a missing "//", a stray quote copied out of a .env file, or a shell-truncated value.) ' +
|
|
581
581
|
'The value is not included here because it may contain a password.');
|
|
582
582
|
}
|
|
@@ -598,7 +598,7 @@ export class MigrationError extends TurbineError {
|
|
|
598
598
|
export class CircularRelationError extends TurbineError {
|
|
599
599
|
path;
|
|
600
600
|
constructor(path) {
|
|
601
|
-
super(TurbineErrorCode.CIRCULAR_RELATION, `
|
|
601
|
+
super(TurbineErrorCode.CIRCULAR_RELATION, `Circular or too-deep relation nesting detected: ${path.join(' → ')}. Maximum nesting depth is 10.`);
|
|
602
602
|
this.name = 'CircularRelationError';
|
|
603
603
|
this.path = path;
|
|
604
604
|
}
|
|
@@ -627,7 +627,7 @@ export class UniqueConstraintError extends TurbineError {
|
|
|
627
627
|
if (!message) {
|
|
628
628
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
629
629
|
const columnsPart = columns && columns.length > 0 ? ` (${columns.join(', ')})` : '';
|
|
630
|
-
message = `
|
|
630
|
+
message = `Unique constraint violation${constraintPart}${columnsPart}`;
|
|
631
631
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
632
632
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
633
633
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
@@ -657,7 +657,7 @@ export class ForeignKeyError extends TurbineError {
|
|
|
657
657
|
let message = opts.message;
|
|
658
658
|
if (!message) {
|
|
659
659
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
660
|
-
message = `
|
|
660
|
+
message = `Foreign key constraint violation${constraintPart}`;
|
|
661
661
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
662
662
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
663
663
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
@@ -686,7 +686,7 @@ export class NotNullViolationError extends TurbineError {
|
|
|
686
686
|
let message = opts.message;
|
|
687
687
|
if (!message) {
|
|
688
688
|
const columnPart = column ? ` on column "${column}"` : '';
|
|
689
|
-
message = `
|
|
689
|
+
message = `NOT NULL constraint violation${columnPart}`;
|
|
690
690
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
691
691
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
692
692
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
@@ -731,7 +731,7 @@ export class DeadlockError extends TurbineError {
|
|
|
731
731
|
let message = opts.message;
|
|
732
732
|
if (!message) {
|
|
733
733
|
const pgMessage = cause?.message;
|
|
734
|
-
message = pgMessage ? `
|
|
734
|
+
message = pgMessage ? `Deadlock detected: ${pgMessage}` : 'Deadlock detected';
|
|
735
735
|
}
|
|
736
736
|
super(TurbineErrorCode.DEADLOCK_DETECTED, message, { cause });
|
|
737
737
|
this.name = 'DeadlockError';
|
|
@@ -763,9 +763,7 @@ export class SerializationFailureError extends TurbineError {
|
|
|
763
763
|
let message = opts.message;
|
|
764
764
|
if (!message) {
|
|
765
765
|
const pgMessage = cause?.message;
|
|
766
|
-
message = pgMessage
|
|
767
|
-
? `[turbine] Serializable transaction conflict: ${pgMessage}`
|
|
768
|
-
: '[turbine] Serializable transaction conflict';
|
|
766
|
+
message = pgMessage ? `Serializable transaction conflict: ${pgMessage}` : 'Serializable transaction conflict';
|
|
769
767
|
}
|
|
770
768
|
super(TurbineErrorCode.SERIALIZATION_FAILURE, message, { cause });
|
|
771
769
|
this.name = 'SerializationFailureError';
|
|
@@ -780,7 +778,7 @@ export class CheckConstraintError extends TurbineError {
|
|
|
780
778
|
let message = opts.message;
|
|
781
779
|
if (!message) {
|
|
782
780
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
783
|
-
message = `
|
|
781
|
+
message = `Check constraint violation${constraintPart}`;
|
|
784
782
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
785
783
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
786
784
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
@@ -808,7 +806,7 @@ export class ExclusionConstraintError extends TurbineError {
|
|
|
808
806
|
let message = opts.message;
|
|
809
807
|
if (!message) {
|
|
810
808
|
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
811
|
-
message = `
|
|
809
|
+
message = `Exclusion constraint violation${constraintPart}`;
|
|
812
810
|
// PII-safe by default: the raw pg `detail` string contains the
|
|
813
811
|
// conflicting row VALUES (e.g. `Key (email)=(alice@x.com) already
|
|
814
812
|
// exists.`). Only append it in 'verbose' mode. In 'safe' mode the
|
|
@@ -859,7 +857,7 @@ export class PipelineError extends TurbineError {
|
|
|
859
857
|
const { results, failedIndex, failedTag, cause } = opts;
|
|
860
858
|
const failedCount = results.filter((r) => r.status === 'error').length;
|
|
861
859
|
const message = opts.message ??
|
|
862
|
-
`
|
|
860
|
+
`Pipeline completed with ${failedCount} error(s) out of ${results.length} queries` +
|
|
863
861
|
(failedTag ? ` (first failure: ${failedTag} at index ${failedIndex})` : '');
|
|
864
862
|
super(TurbineErrorCode.PIPELINE, message, { cause });
|
|
865
863
|
this.name = 'PipelineError';
|
|
@@ -873,7 +871,7 @@ export class OptimisticLockError extends TurbineError {
|
|
|
873
871
|
versionField;
|
|
874
872
|
expectedVersion;
|
|
875
873
|
constructor(opts) {
|
|
876
|
-
super(TurbineErrorCode.OPTIMISTIC_LOCK, `
|
|
874
|
+
super(TurbineErrorCode.OPTIMISTIC_LOCK, `Optimistic lock failed on "${opts.table}", ` +
|
|
877
875
|
`expected ${opts.versionField} = ${opts.expectedVersion} but row was modified by another transaction`);
|
|
878
876
|
this.name = 'OptimisticLockError';
|
|
879
877
|
this.table = opts.table;
|
|
@@ -891,7 +889,7 @@ export class UnsupportedFeatureError extends TurbineError {
|
|
|
891
889
|
feature;
|
|
892
890
|
dialect;
|
|
893
891
|
constructor(feature, dialect, hint) {
|
|
894
|
-
super(TurbineErrorCode.UNSUPPORTED_FEATURE,
|
|
892
|
+
super(TurbineErrorCode.UNSUPPORTED_FEATURE, `${feature} is unsupported on "${dialect}".${hint ? ` ${hint}` : ''}`);
|
|
895
893
|
this.name = 'UnsupportedFeatureError';
|
|
896
894
|
this.feature = feature;
|
|
897
895
|
this.dialect = dialect;
|
|
@@ -928,7 +926,7 @@ export class ReadOnlyError extends TurbineError {
|
|
|
928
926
|
* and the refusal `reason` (default `'snapshot'`).
|
|
929
927
|
*/
|
|
930
928
|
constructor(detail, options) {
|
|
931
|
-
super(TurbineErrorCode.READ_ONLY,
|
|
929
|
+
super(TurbineErrorCode.READ_ONLY, `${detail} Route writes to a writable primary.`, {
|
|
932
930
|
cause: options?.cause,
|
|
933
931
|
});
|
|
934
932
|
this.name = 'ReadOnlyError';
|
|
@@ -1101,7 +1099,7 @@ export function wrapPgError(err) {
|
|
|
1101
1099
|
// Turbine did not set the deadline (that lives in Postgres config), so
|
|
1102
1100
|
// there is no client-side budget to report → timeoutMs = 0.
|
|
1103
1101
|
return new TimeoutError(0, 'Query', {
|
|
1104
|
-
message: '
|
|
1102
|
+
message: 'Query canceled by server-side statement_timeout',
|
|
1105
1103
|
cause: err,
|
|
1106
1104
|
});
|
|
1107
1105
|
default:
|
|
@@ -1111,9 +1109,7 @@ export function wrapPgError(err) {
|
|
|
1111
1109
|
// plain-object map would happily resolve `constructor` or `toString`
|
|
1112
1110
|
// to a function and interpolate it into the message.
|
|
1113
1111
|
const hint = Object.hasOwn(CONNECTION_ERROR_HINTS, e.code) ? CONNECTION_ERROR_HINTS[e.code] : undefined;
|
|
1114
|
-
const head = pgMessage
|
|
1115
|
-
? `[turbine] Database connection error: ${pgMessage}`
|
|
1116
|
-
: `[turbine] Database connection error (${e.code})`;
|
|
1112
|
+
const head = pgMessage ? `Database connection error: ${pgMessage}` : `Database connection error (${e.code})`;
|
|
1117
1113
|
return new ConnectionError(hint ? `${head} (${e.code}) ${hint}` : head, { cause: err, sqlstate: e.code });
|
|
1118
1114
|
}
|
|
1119
1115
|
return err;
|
package/dist/generate.js
CHANGED
|
@@ -363,7 +363,7 @@ export function isEmittableIdentifier(name) {
|
|
|
363
363
|
function requireEmittable(derived, subject, role) {
|
|
364
364
|
if (isEmittableIdentifier(derived))
|
|
365
365
|
return;
|
|
366
|
-
throw new ValidationError(`
|
|
366
|
+
throw new ValidationError(`Cannot generate code for ${subject}: it produces ${JSON.stringify(derived)} as ${role}, ` +
|
|
367
367
|
`which is not a valid TypeScript identifier. Rename the database object, or exclude it from generation ` +
|
|
368
368
|
`(introspect \`exclude\`).`);
|
|
369
369
|
}
|
package/dist/introspect.js
CHANGED
|
@@ -337,7 +337,7 @@ export function assertSafeCatalogIdentifier(name, subject) {
|
|
|
337
337
|
if (match === null)
|
|
338
338
|
return;
|
|
339
339
|
const code = match[0].charCodeAt(0).toString(16).padStart(4, '0');
|
|
340
|
-
throw new ValidationError(`
|
|
340
|
+
throw new ValidationError(`Refusing to introspect ${subject}: its name contains the control character U+${code.toUpperCase()} ` +
|
|
341
341
|
`at position ${match.index}. Such a name cannot be safely emitted into generated code, SQL comments, or ` +
|
|
342
342
|
`tooling output. Rename the database object, or exclude it from introspection.`);
|
|
343
343
|
}
|
|
@@ -434,22 +434,22 @@ export function applyRelationRenames(schema, renames) {
|
|
|
434
434
|
for (const [table, mapping] of Object.entries(renames)) {
|
|
435
435
|
const meta = tables[table];
|
|
436
436
|
if (!meta) {
|
|
437
|
-
throw new ValidationError(`
|
|
437
|
+
throw new ValidationError(`relationNames: unknown table "${table}". Known tables: ${Object.keys(tables).join(', ')}.`);
|
|
438
438
|
}
|
|
439
439
|
const relations = { ...meta.relations };
|
|
440
440
|
const columnFields = new Set(Object.values(meta.columnMap));
|
|
441
441
|
for (const [from, to] of Object.entries(mapping)) {
|
|
442
442
|
if (!Object.hasOwn(relations, from)) {
|
|
443
|
-
throw new ValidationError(`
|
|
443
|
+
throw new ValidationError(`relationNames: table "${table}" has no relation "${from}". ` +
|
|
444
444
|
`Derived relations: ${Object.keys(relations).join(', ') || '(none)'}.`);
|
|
445
445
|
}
|
|
446
446
|
if (from === to)
|
|
447
447
|
continue;
|
|
448
448
|
if (Object.hasOwn(relations, to)) {
|
|
449
|
-
throw new ValidationError(`
|
|
449
|
+
throw new ValidationError(`relationNames: cannot rename "${table}.${from}" to "${to}", that relation already exists.`);
|
|
450
450
|
}
|
|
451
451
|
if (columnFields.has(to)) {
|
|
452
|
-
throw new ValidationError(`
|
|
452
|
+
throw new ValidationError(`relationNames: cannot rename "${table}.${from}" to "${to}", a column on "${table}" ` +
|
|
453
453
|
`already uses that field name, and a relation must never shadow a column.`);
|
|
454
454
|
}
|
|
455
455
|
const def = relations[from];
|
package/dist/mssql.js
CHANGED
|
@@ -652,11 +652,11 @@ export const mssqlDialect = {
|
|
|
652
652
|
const rowCount = input.rowValues.length;
|
|
653
653
|
const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
|
|
654
654
|
if (rowCount > MSSQL_MAX_INSERT_ROWS) {
|
|
655
|
-
throw new ValidationError(`
|
|
655
|
+
throw new ValidationError(`SQL Server INSERT … VALUES is limited to ${MSSQL_MAX_INSERT_ROWS} rows per statement (got ${rowCount}). ` +
|
|
656
656
|
'Chunk the data or use individual create() calls.');
|
|
657
657
|
}
|
|
658
658
|
if (paramCount > MSSQL_MAX_PARAMS) {
|
|
659
|
-
throw new ValidationError(`
|
|
659
|
+
throw new ValidationError(`SQL Server is limited to ${MSSQL_MAX_PARAMS} bound parameters per statement (got ${paramCount}). ` +
|
|
660
660
|
'Reduce the batch size.');
|
|
661
661
|
}
|
|
662
662
|
let n = 0;
|
|
@@ -904,7 +904,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
904
904
|
for (const [nestedRelName, nestedSpec] of sortedRelEntries(spec.with)) {
|
|
905
905
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
906
906
|
if (!nestedRelDef) {
|
|
907
|
-
throw new RelationError(`
|
|
907
|
+
throw new RelationError(`Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
908
908
|
availableClause(Object.keys(targetMeta.relations), 'It has no relations.'));
|
|
909
909
|
}
|
|
910
910
|
const sub = ctx.recurse(nestedRelDef, nestedSpec, parentAlias, depth + 1, [...path, relDef.name]);
|
|
@@ -926,7 +926,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
926
926
|
if (typeof dir === 'object' && dir !== null) {
|
|
927
927
|
const sortValue = dir.sort;
|
|
928
928
|
if (typeof sortValue !== 'string') {
|
|
929
|
-
throw new ValidationError(`
|
|
929
|
+
throw new ValidationError(`Nested orderBy on "${k}" (table "${targetTable}"): only plain directions and ` +
|
|
930
930
|
`{ sort } specs are supported inside a relation orderBy on SQL Server.`);
|
|
931
931
|
}
|
|
932
932
|
if (dir.nulls !== undefined) {
|
|
@@ -939,7 +939,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
939
939
|
// resolve on SQL Server too.
|
|
940
940
|
const col = targetMeta.columnMap[k] ?? camelToSnake(k);
|
|
941
941
|
if (!targetMeta.allColumns.includes(col)) {
|
|
942
|
-
throw new ValidationError(`
|
|
942
|
+
throw new ValidationError(`Unknown field "${k}" in orderBy on table "${targetTable}". ` +
|
|
943
943
|
`Known fields: ${Object.keys(targetMeta.columnMap).join(', ') || '(none)'}.`);
|
|
944
944
|
}
|
|
945
945
|
const safeDir = String(rawDir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
@@ -1005,7 +1005,7 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1005
1005
|
const { relDef, spec, params, parentRef, alias, targetTable, targetMeta } = ctx;
|
|
1006
1006
|
const q = (name) => dialect.quoteIdentifier(name);
|
|
1007
1007
|
if (!relDef.through) {
|
|
1008
|
-
throw new ValidationError(`
|
|
1008
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
1009
1009
|
}
|
|
1010
1010
|
const qTarget = q(targetTable);
|
|
1011
1011
|
const qJunction = q(relDef.through.table);
|
|
@@ -1014,12 +1014,12 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1014
1014
|
// JOIN: junction.targetKey = target.<PK>.
|
|
1015
1015
|
const targetKeys = normalizeKeyColumns(relDef.through.targetKey);
|
|
1016
1016
|
if (targetMeta.primaryKey.length === 0) {
|
|
1017
|
-
throw new ValidationError(`
|
|
1017
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
1018
1018
|
'cannot determine the join column.');
|
|
1019
1019
|
}
|
|
1020
1020
|
const targetPk = targetMeta.primaryKey;
|
|
1021
1021
|
if (targetKeys.length !== targetPk.length) {
|
|
1022
|
-
throw new ValidationError(`
|
|
1022
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
|
|
1023
1023
|
`but target "${targetTable}" primary key has ${targetPk.length}.`);
|
|
1024
1024
|
}
|
|
1025
1025
|
const joinOn = targetKeys.map((jcol, i) => `${jalias}.${q(jcol)} = ${alias}.${q(targetPk[i])}`).join(' AND ');
|
|
@@ -1027,7 +1027,7 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1027
1027
|
const sourceKeys = normalizeKeyColumns(relDef.through.sourceKey);
|
|
1028
1028
|
const refKeys = normalizeKeyColumns(relDef.referenceKey);
|
|
1029
1029
|
if (sourceKeys.length !== refKeys.length) {
|
|
1030
|
-
throw new ValidationError(`
|
|
1030
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
|
|
1031
1031
|
`but referenceKey has ${refKeys.length}.`);
|
|
1032
1032
|
}
|
|
1033
1033
|
const correlation = sourceKeys.map((jcol, i) => `${jalias}.${q(jcol)} = ${qParent}.${q(refKeys[i])}`).join(' AND ');
|
|
@@ -1309,13 +1309,13 @@ async function loadMssql() {
|
|
|
1309
1309
|
mod = (await importOptionalPeer('mssql'));
|
|
1310
1310
|
}
|
|
1311
1311
|
catch (err) {
|
|
1312
|
-
throw new ConnectionError("
|
|
1312
|
+
throw new ConnectionError("turbine-orm/mssql requires the optional peer dependency 'mssql'. Install it: npm i mssql. " +
|
|
1313
1313
|
`(${err.message})`);
|
|
1314
1314
|
}
|
|
1315
1315
|
// `mssql` is a CommonJS module; the namespace may be on `default` under ESM interop.
|
|
1316
1316
|
const ns = (mod.default ?? mod);
|
|
1317
1317
|
if (typeof ns.ConnectionPool !== 'function' || typeof ns.Request !== 'function') {
|
|
1318
|
-
throw new ConnectionError("
|
|
1318
|
+
throw new ConnectionError("Loaded 'mssql' but it is missing ConnectionPool/Request exports.");
|
|
1319
1319
|
}
|
|
1320
1320
|
return ns;
|
|
1321
1321
|
}
|
|
@@ -1325,7 +1325,7 @@ async function loadMssql() {
|
|
|
1325
1325
|
*/
|
|
1326
1326
|
function assertSupportedVersion(majorVersion) {
|
|
1327
1327
|
if (majorVersion > 0 && majorVersion < 13) {
|
|
1328
|
-
throw new ConnectionError(`
|
|
1328
|
+
throw new ConnectionError(`turbine-orm/mssql requires SQL Server 2016+ (FOR JSON / OPENJSON); got major version ${majorVersion}.`);
|
|
1329
1329
|
}
|
|
1330
1330
|
}
|
|
1331
1331
|
function isMssqlPool(x) {
|
package/dist/mysql.js
CHANGED
|
@@ -1015,7 +1015,7 @@ export async function introspectMysql(options) {
|
|
|
1015
1015
|
schemaName = rows[0]?.db ?? '';
|
|
1016
1016
|
}
|
|
1017
1017
|
if (!schemaName) {
|
|
1018
|
-
throw new ConnectionError('
|
|
1018
|
+
throw new ConnectionError('Could not determine the MySQL database to introspect, pass a database in the connection string or a `schema` option.');
|
|
1019
1019
|
}
|
|
1020
1020
|
// `await` is LOAD-BEARING, not stylistic: `return somePromise` inside a
|
|
1021
1021
|
// try/finally runs the finally BEFORE the promise settles, so `pool.end()`
|
|
@@ -1090,12 +1090,12 @@ async function loadCreatePool() {
|
|
|
1090
1090
|
mod = (await importOptionalPeer('mysql2/promise'));
|
|
1091
1091
|
}
|
|
1092
1092
|
catch (err) {
|
|
1093
|
-
throw new ConnectionError("
|
|
1093
|
+
throw new ConnectionError("turbine-orm/mysql requires the optional peer dependency 'mysql2'. Install it: npm i mysql2. " +
|
|
1094
1094
|
`(${err.message})`);
|
|
1095
1095
|
}
|
|
1096
1096
|
const createPool = mod.createPool ?? mod.default?.createPool;
|
|
1097
1097
|
if (typeof createPool !== 'function') {
|
|
1098
|
-
throw new ConnectionError("
|
|
1098
|
+
throw new ConnectionError("Loaded 'mysql2/promise' but it has no createPool export.");
|
|
1099
1099
|
}
|
|
1100
1100
|
return createPool;
|
|
1101
1101
|
}
|
|
@@ -1105,12 +1105,12 @@ async function loadCreatePool() {
|
|
|
1105
1105
|
*/
|
|
1106
1106
|
function assertSupportedVersion(version) {
|
|
1107
1107
|
if (/mariadb/i.test(version)) {
|
|
1108
|
-
throw new ConnectionError(`
|
|
1108
|
+
throw new ConnectionError(`MariaDB is not supported by turbine-orm/mysql (got "${version}"). Use MySQL 8.0+.`);
|
|
1109
1109
|
}
|
|
1110
1110
|
const m = /^(\d+)\.(\d+)/.exec(version);
|
|
1111
1111
|
const major = m ? Number(m[1]) : 0;
|
|
1112
1112
|
if (major < 8) {
|
|
1113
|
-
throw new ConnectionError(`
|
|
1113
|
+
throw new ConnectionError(`turbine-orm/mysql requires MySQL 8.0+ (5.7 lacks JSON_ARRAYAGG); got "${version}".`);
|
|
1114
1114
|
}
|
|
1115
1115
|
}
|
|
1116
1116
|
function isMysql2Pool(x) {
|