turbine-orm 0.32.0 → 0.32.2
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 +28 -19
- package/dist/cjs/cli/config.js +76 -16
- package/dist/cjs/cli/index.js +174 -19
- package/dist/cjs/client.js +16 -0
- package/dist/cjs/errors.js +62 -4
- package/dist/cjs/generate.js +2 -1
- package/dist/cjs/powql.js +12 -1
- package/dist/cjs/query/builder.js +332 -44
- package/dist/cli/config.d.ts +53 -1
- package/dist/cli/config.js +73 -16
- package/dist/cli/index.d.ts +68 -0
- package/dist/cli/index.js +173 -21
- package/dist/client.js +16 -0
- package/dist/errors.d.ts +22 -2
- package/dist/errors.js +62 -4
- package/dist/generate.js +2 -1
- package/dist/powql.js +12 -1
- package/dist/query/builder.d.ts +57 -0
- package/dist/query/builder.js +332 -44
- package/dist/query/deferred.d.ts +10 -1
- package/dist/query/types.d.ts +40 -2
- package/package.json +3 -2
package/dist/errors.js
CHANGED
|
@@ -144,8 +144,16 @@ export class NotFoundError extends TurbineError {
|
|
|
144
144
|
/** Thrown when a query or transaction exceeds the configured timeout */
|
|
145
145
|
export class TimeoutError extends TurbineError {
|
|
146
146
|
timeoutMs;
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
/**
|
|
148
|
+
* @param timeoutMs the client-side timeout budget in ms. Pass `0` when the
|
|
149
|
+
* duration is unknown (e.g. a server-side `statement_timeout` cancellation
|
|
150
|
+
* surfaced via `wrapPgError`, where Turbine did not set the deadline).
|
|
151
|
+
* @param context human label for the operation ("Query", "Transaction").
|
|
152
|
+
* @param options optional `message` override and pg `cause` to preserve, used
|
|
153
|
+
* when wrapping a driver error rather than a client-side timer expiry.
|
|
154
|
+
*/
|
|
155
|
+
constructor(timeoutMs, context = 'Query', options) {
|
|
156
|
+
super(TurbineErrorCode.TIMEOUT, options?.message ?? `[turbine] ${context} timed out after ${timeoutMs}ms`, options);
|
|
149
157
|
this.name = 'TimeoutError';
|
|
150
158
|
this.timeoutMs = timeoutMs;
|
|
151
159
|
}
|
|
@@ -159,8 +167,13 @@ export class ValidationError extends TurbineError {
|
|
|
159
167
|
}
|
|
160
168
|
/** Thrown when a database connection fails */
|
|
161
169
|
export class ConnectionError extends TurbineError {
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
/**
|
|
171
|
+
* @param message human-readable connection failure description.
|
|
172
|
+
* @param options optional pg/driver `cause` to preserve, used when wrapping a
|
|
173
|
+
* connection-class driver error via `wrapPgError`.
|
|
174
|
+
*/
|
|
175
|
+
constructor(message, options) {
|
|
176
|
+
super(TurbineErrorCode.CONNECTION, message, options);
|
|
164
177
|
this.name = 'ConnectionError';
|
|
165
178
|
}
|
|
166
179
|
}
|
|
@@ -477,6 +490,35 @@ function parseColumnsFromDetail(detail) {
|
|
|
477
490
|
return undefined;
|
|
478
491
|
return m[1].split(',').map((s) => s.trim());
|
|
479
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Connection-class error codes. Covers both pg SQLSTATEs (class 08
|
|
495
|
+
* connection_exception, plus a few class-53/57 admin/availability codes) and
|
|
496
|
+
* Node driver-level error codes that arrive on the same `.code` field when the
|
|
497
|
+
* socket never reaches Postgres. All map to {@link ConnectionError} (E004).
|
|
498
|
+
*
|
|
499
|
+
* `57014` (query_canceled, a server-side `statement_timeout` cancellation) is
|
|
500
|
+
* intentionally NOT here: it maps to {@link TimeoutError} (E002) instead.
|
|
501
|
+
*/
|
|
502
|
+
const CONNECTION_ERROR_CODES = new Set([
|
|
503
|
+
// pg SQLSTATE class 08: connection_exception
|
|
504
|
+
'08000', // connection_exception
|
|
505
|
+
'08001', // sqlclient_unable_to_establish_sqlconnection
|
|
506
|
+
'08003', // connection_does_not_exist
|
|
507
|
+
'08004', // sqlserver_rejected_establishment_of_sqlconnection
|
|
508
|
+
'08006', // connection_failure
|
|
509
|
+
'08P01', // protocol_violation
|
|
510
|
+
// pg SQLSTATE class 53/57 (server unavailable / shutting down)
|
|
511
|
+
'53300', // too_many_connections
|
|
512
|
+
'57P01', // admin_shutdown
|
|
513
|
+
'57P02', // crash_shutdown
|
|
514
|
+
'57P03', // cannot_connect_now
|
|
515
|
+
// Node driver-level socket errors (surface on err.code too)
|
|
516
|
+
'ECONNREFUSED',
|
|
517
|
+
'ECONNRESET',
|
|
518
|
+
'ETIMEDOUT',
|
|
519
|
+
'ENOTFOUND',
|
|
520
|
+
'EPIPE',
|
|
521
|
+
]);
|
|
480
522
|
/**
|
|
481
523
|
* Translate a pg driver error into a typed Turbine error.
|
|
482
524
|
* If the error doesn't match a known constraint code, returns it unchanged.
|
|
@@ -489,6 +531,8 @@ function parseColumnsFromDetail(detail) {
|
|
|
489
531
|
* 23P01 (exclusion_violation) -> ExclusionConstraintError
|
|
490
532
|
* 40P01 (deadlock_detected) -> DeadlockError (retryable)
|
|
491
533
|
* 40001 (serialization_failure) -> SerializationFailureError (retryable)
|
|
534
|
+
* 57014 (query_canceled) -> TimeoutError (server-side statement_timeout)
|
|
535
|
+
* connection-class codes -> ConnectionError (see CONNECTION_ERROR_CODES)
|
|
492
536
|
*
|
|
493
537
|
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
494
538
|
*/
|
|
@@ -541,7 +585,21 @@ export function wrapPgError(err) {
|
|
|
541
585
|
return new SerializationFailureError({
|
|
542
586
|
cause: err,
|
|
543
587
|
});
|
|
588
|
+
case '57014':
|
|
589
|
+
// query_canceled: a server-side statement_timeout cancelled the query.
|
|
590
|
+
// Turbine did not set the deadline (that lives in Postgres config), so
|
|
591
|
+
// there is no client-side budget to report → timeoutMs = 0.
|
|
592
|
+
return new TimeoutError(0, 'Query', {
|
|
593
|
+
message: '[turbine] Query canceled by server-side statement_timeout',
|
|
594
|
+
cause: err,
|
|
595
|
+
});
|
|
544
596
|
default:
|
|
597
|
+
if (CONNECTION_ERROR_CODES.has(e.code)) {
|
|
598
|
+
const pgMessage = typeof e.message === 'string' && e.message.length > 0 ? e.message : undefined;
|
|
599
|
+
return new ConnectionError(pgMessage
|
|
600
|
+
? `[turbine] Database connection error: ${pgMessage}`
|
|
601
|
+
: `[turbine] Database connection error (${e.code})`, { cause: err });
|
|
602
|
+
}
|
|
545
603
|
return err;
|
|
546
604
|
}
|
|
547
605
|
}
|
package/dist/generate.js
CHANGED
|
@@ -670,7 +670,8 @@ export function generateIndex(schema, options) {
|
|
|
670
670
|
lines.push('/**');
|
|
671
671
|
lines.push(' * Create a new Turbine client instance.');
|
|
672
672
|
lines.push(' *');
|
|
673
|
-
lines.push(' * @param config - Connection configuration.
|
|
673
|
+
lines.push(' * @param config - Connection configuration. Omit it (or pass no connection');
|
|
674
|
+
lines.push(' * fields) to fall back to the `DATABASE_URL` environment variable.');
|
|
674
675
|
lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
|
|
675
676
|
lines.push(' */');
|
|
676
677
|
lines.push('export function turbine(config?: TurbineConfig): TurbineClient {');
|
package/dist/powql.js
CHANGED
|
@@ -582,7 +582,7 @@ export class PowqlInterface {
|
|
|
582
582
|
const limit = args.limit ?? args.take ?? this.defaultLimit;
|
|
583
583
|
if (limit === undefined && this.warnOnUnlimited && !this.warnedUnlimited) {
|
|
584
584
|
this.warnedUnlimited = true;
|
|
585
|
-
console.warn(`[turbine] findMany on "${this.table}" has no limit
|
|
585
|
+
console.warn(`[turbine] findMany on "${this.table}" has no limit: this scans the whole table.`);
|
|
586
586
|
}
|
|
587
587
|
const limitClause = limit !== undefined ? ` limit ${this.param(limit, params)}` : '';
|
|
588
588
|
const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
|
|
@@ -1180,6 +1180,17 @@ export class PowqlInterface {
|
|
|
1180
1180
|
proj.push(`${a.alias}: ${a.fn}(${a.field ? this.ref(a.field) : `.${this.meta.primaryKey[0]}`})`);
|
|
1181
1181
|
}
|
|
1182
1182
|
const having = this.buildHaving(args.having, params);
|
|
1183
|
+
// groupBy aggregate ordering (`_count` / `_sum` / … keys) has no PowQL
|
|
1184
|
+
// equivalent here: `buildOrder` treats an `orderBy` key as a field ref, so
|
|
1185
|
+
// a bare `_count: 'desc'` would silently emit an invalid `._count` sort.
|
|
1186
|
+
// Refuse those keys explicitly; plain by-field ordering still flows through.
|
|
1187
|
+
if (args.orderBy) {
|
|
1188
|
+
for (const key of Object.keys(args.orderBy)) {
|
|
1189
|
+
if (key === '_count' || key === '_sum' || key === '_avg' || key === '_min' || key === '_max') {
|
|
1190
|
+
throw new UnsupportedFeatureError('groupBy ordering by an aggregate', 'PowDB', `orderBy key "${key}"`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1183
1194
|
const order = this.buildOrder(args.orderBy);
|
|
1184
1195
|
const powql = `${this.qt}${filter} group ${groupKeys.join(', ')}${having}${order} { ${proj.join(', ')} }`;
|
|
1185
1196
|
const { rows } = await this.exec(powql, params, args.timeout);
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
22
22
|
private readonly tableMeta;
|
|
23
23
|
/** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
|
|
24
24
|
private readonly sqlTemplateCache;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the most recent {@link acquireSql} call was a cache HIT. Read by
|
|
27
|
+
* {@link crossCheckCache} to decide whether to run the dev-mode lockstep
|
|
28
|
+
* cross-check. Safe as a single mutable flag: each `build*()` method calls
|
|
29
|
+
* `acquireSql` then `crossCheckCache` synchronously with no intervening
|
|
30
|
+
* `await` and no re-entrant `acquireSql` (relation subqueries are built
|
|
31
|
+
* inline, not through the top-level cache).
|
|
32
|
+
*/
|
|
33
|
+
private lastCacheHit;
|
|
25
34
|
private readonly middlewares;
|
|
26
35
|
private readonly defaultLimit?;
|
|
27
36
|
private readonly warnOnUnlimited;
|
|
@@ -183,8 +192,42 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
183
192
|
* On hit, increments counters and returns the cached entry.
|
|
184
193
|
*
|
|
185
194
|
* When `sqlCache` is disabled, always calls `build()` without caching.
|
|
195
|
+
*
|
|
196
|
+
* `build` receives a fresh `$N` param scratch array. On a miss those params
|
|
197
|
+
* are discarded (the returned params come from each call site's dedicated
|
|
198
|
+
* collect path); the array exists so the build path can number placeholders
|
|
199
|
+
* via `params.length` exactly as it does today. On a HIT, `build` is skipped
|
|
200
|
+
* here but re-run by {@link crossCheckCache} (dev only) with a fresh array to
|
|
201
|
+
* verify the collect path stayed in lockstep with the build path.
|
|
202
|
+
*
|
|
203
|
+
* Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows whether a
|
|
204
|
+
* cross-check is warranted.
|
|
186
205
|
*/
|
|
187
206
|
private acquireSql;
|
|
207
|
+
/**
|
|
208
|
+
* Dev-mode SQL-cache lockstep cross-check (see {@link cacheCrossCheckEnabled}).
|
|
209
|
+
*
|
|
210
|
+
* Runs only when the most recent {@link acquireSql} was a cache HIT and the
|
|
211
|
+
* check is enabled. Rebuilds the SQL + `$N` params fresh via the same `build`
|
|
212
|
+
* closure the caller passed to `acquireSql`, then compares:
|
|
213
|
+
* (a) the cached SQL string byte-for-byte against the fresh SQL, and
|
|
214
|
+
* (b) the params the cache-hit collect path produced against the fresh
|
|
215
|
+
* build-path params (length and element-wise strict deep-equal).
|
|
216
|
+
*
|
|
217
|
+
* A mismatch means the fingerprint / build / collect paths have drifted out
|
|
218
|
+
* of lockstep (the exact class of bug that has silently corrupted results
|
|
219
|
+
* before), so it throws a {@link ValidationError} (E003) naming the
|
|
220
|
+
* fingerprint, the operation, and both SQL strings (truncated). Failing loud
|
|
221
|
+
* in dev/test is the point. Production never reaches the comparison.
|
|
222
|
+
*
|
|
223
|
+
* @param op human label of the calling build method (for the error message).
|
|
224
|
+
* @param cacheKey the cache fingerprint that HIT.
|
|
225
|
+
* @param entry the cached SQL entry that will be executed.
|
|
226
|
+
* @param build the same closure passed to `acquireSql`; re-run here to
|
|
227
|
+
* capture the fresh build-path SQL + params.
|
|
228
|
+
* @param collectedParams the params the caller's collect path produced.
|
|
229
|
+
*/
|
|
230
|
+
private crossCheckCache;
|
|
188
231
|
/**
|
|
189
232
|
* Reset the per-instance unlimited-query warning dedupe set.
|
|
190
233
|
* Exposed for tests so a single test process can verify the warning fires
|
|
@@ -339,6 +382,20 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
339
382
|
buildCount(args?: CountArgs<T>): DeferredQuery<number>;
|
|
340
383
|
groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
|
|
341
384
|
buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
|
|
385
|
+
/**
|
|
386
|
+
* Compile a groupBy `orderBy` into an ORDER BY body. Unlike findMany ORDER BY
|
|
387
|
+
* ({@link buildOrderBy}, which validates keys against the table's physical
|
|
388
|
+
* columns), groupBy ordering targets the columns the RESULT actually
|
|
389
|
+
* contains: plain by-fields, JSON group-key aliases, and requested aggregates
|
|
390
|
+
* (`_count` / `_sum` / `_avg` / `_min` / `_max`). Each key re-emits the exact
|
|
391
|
+
* SELECT expression that produced it (`byOrderExprs` / `aggOrderExprs`),
|
|
392
|
+
* mirroring how HAVING re-emits aggregate expressions, so no dialect ever has
|
|
393
|
+
* to accept a SELECT-alias reference in ORDER BY, and any already-bound
|
|
394
|
+
* JSON-path placeholder is reused verbatim (ORDER BY is the last clause, so
|
|
395
|
+
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
396
|
+
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
397
|
+
*/
|
|
398
|
+
private buildGroupByOrderBy;
|
|
342
399
|
/**
|
|
343
400
|
* Validate a JSON-path target (group key or aggregate target) in groupBy:
|
|
344
401
|
* the field must resolve to a real json/jsonb column and the path must be a
|