turbine-orm 0.30.0 → 0.32.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.
@@ -7,9 +7,10 @@
7
7
  * and execution rather than filter-shape bookkeeping.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.VECTOR_DISTANCE_COMPARATORS = exports.VECTOR_METRIC_OPERATORS = exports.TEXT_SEARCH_KEYS = exports.ARRAY_UNIQUE_KEYS = exports.ARRAY_OPERATOR_KEYS = exports.JSONB_UNIQUE_KEYS = exports.JSON_RANGE_OPERATORS = exports.JSONB_OPERATOR_KEYS = exports.UPDATE_OPERATOR_KEYS = void 0;
10
+ exports.VECTOR_DISTANCE_COMPARATORS = exports.VECTOR_METRIC_OPERATORS = exports.TEXT_SEARCH_KEYS = exports.ARRAY_UNIQUE_KEYS = exports.ARRAY_OPERATOR_KEYS = exports.JSONB_UNIQUE_KEYS = exports.JSON_RANGE_OPERATORS = exports.JSONB_OPERATOR_KEYS = exports.UPDATE_OPERATOR_KEYS = exports.COLUMN_REF_OPERATORS = void 0;
11
11
  exports.isWhereOperator = isWhereOperator;
12
12
  exports.isUnmatchedPlainObject = isUnmatchedPlainObject;
13
+ exports.isColumnRef = isColumnRef;
13
14
  exports.fingerprintOperatorShape = fingerprintOperatorShape;
14
15
  exports.assertBindableEqualsOperand = assertBindableEqualsOperand;
15
16
  exports.sortedKeys = sortedKeys;
@@ -24,6 +25,8 @@ exports.validateTextSearchConfig = validateTextSearchConfig;
24
25
  exports.isVectorFilter = isVectorFilter;
25
26
  exports.isVectorOrderBy = isVectorOrderBy;
26
27
  exports.isOrderBySpec = isOrderBySpec;
28
+ exports.isJsonPathOrderBy = isJsonPathOrderBy;
29
+ exports.isRelationPickOrderBy = isRelationPickOrderBy;
27
30
  exports.normalizeOrderBy = normalizeOrderBy;
28
31
  const errors_js_1 = require("../errors.js");
29
32
  const utils_js_1 = require("./utils.js");
@@ -56,17 +59,48 @@ function isUnmatchedPlainObject(value) {
56
59
  const proto = Object.getPrototypeOf(value);
57
60
  return proto === Object.prototype || proto === null;
58
61
  }
62
+ /**
63
+ * Operator keys that accept a {@link ColumnRef} (`{ col: 'otherField' }`)
64
+ * value for column-to-column comparison. `in`/`notIn` and the LIKE operators
65
+ * take values only.
66
+ */
67
+ exports.COLUMN_REF_OPERATORS = new Set(['equals', 'not', 'gt', 'gte', 'lt', 'lte']);
68
+ /**
69
+ * Check if an operator value is a column reference: a plain object whose ONLY
70
+ * key is `col` with a string value. Anything else (extra keys, non-string
71
+ * `col`) is treated as a plain value so JSON payloads that merely contain a
72
+ * `col` property keep their equality meaning.
73
+ */
74
+ function isColumnRef(value) {
75
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
76
+ return false;
77
+ const keys = Object.keys(value);
78
+ return keys.length === 1 && keys[0] === 'col' && typeof value.col === 'string';
79
+ }
59
80
  /**
60
81
  * Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
61
82
  * `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
62
83
  * param pushed), so null-ness is part of the shape — without it a cache entry
63
84
  * warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
85
+ *
86
+ * Column references ({@link ColumnRef}) compile the referenced column into the
87
+ * SQL TEXT (no param bound), so the referenced field name is part of the shape
88
+ *: `{ equals: { col: 'a' } }` and `{ equals: { col: 'b' } }` must never share
89
+ * a cache entry. The name is JSON-encoded so exotic field names cannot collide
90
+ * with other fingerprint tokens.
64
91
  */
65
92
  function fingerprintOperatorShape(value) {
66
93
  const obj = value;
67
94
  const opKeys = Object.keys(obj)
68
95
  .filter((k) => k !== 'mode')
69
- .map((k) => ((k === 'equals' || k === 'not') && obj[k] === null ? `${k}:null` : k))
96
+ .map((k) => {
97
+ const v = obj[k];
98
+ if ((k === 'equals' || k === 'not') && v === null)
99
+ return `${k}:null`;
100
+ if (exports.COLUMN_REF_OPERATORS.has(k) && isColumnRef(v))
101
+ return `${k}:col(${JSON.stringify(v.col)})`;
102
+ return k;
103
+ })
70
104
  .sort();
71
105
  const modeStr = value.mode === 'insensitive' ? ':i' : '';
72
106
  return `op(${opKeys.join(',')}${modeStr})`;
@@ -269,6 +303,47 @@ function isVectorOrderBy(value) {
269
303
  function isOrderBySpec(value) {
270
304
  return typeof value === 'object' && value !== null && !Array.isArray(value) && 'sort' in value;
271
305
  }
306
+ /**
307
+ * Check if an orderBy value is a JSON-path ordering: `{ path: [...] }` with an
308
+ * ARRAY path. The array requirement disambiguates from relation orderBy values
309
+ * (whose entries are directions/specs keyed by target column: a target column
310
+ * literally named `path` maps to a string direction, never an array), and the
311
+ * `distance`/`sort` exclusions keep vector and spec shapes out.
312
+ */
313
+ function isJsonPathOrderBy(value) {
314
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
315
+ return false;
316
+ if ('distance' in value || 'sort' in value)
317
+ return false;
318
+ return Array.isArray(value.path);
319
+ }
320
+ /**
321
+ * Check if an orderBy value is a pick-row relation ordering:
322
+ * `{ pick: { orderBy, ... }, by, direction?, nulls? }`. The full shape is
323
+ * required — `pick` must be an object carrying `orderBy`, `by` must be
324
+ * present, and no keys outside `{ pick, by, direction, nulls }` — so a to-one
325
+ * relation whose target has real columns literally named `pick` and `by`
326
+ * (whose values are direction strings or `{ sort, nulls }` specs, never an
327
+ * object with `orderBy`) still falls through to column ordering. `distance`
328
+ * (vector), `sort` (OrderBySpec), and a top-level array `path` (JSON-path
329
+ * ordering) are excluded up front.
330
+ */
331
+ function isRelationPickOrderBy(value) {
332
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
333
+ return false;
334
+ if ('distance' in value || 'sort' in value || Array.isArray(value.path))
335
+ return false;
336
+ const v = value;
337
+ if (!('pick' in v) || !('by' in v))
338
+ return false;
339
+ if (typeof v.pick !== 'object' || v.pick === null || Array.isArray(v.pick) || !('orderBy' in v.pick))
340
+ return false;
341
+ for (const key of Object.keys(v)) {
342
+ if (key !== 'pick' && key !== 'by' && key !== 'direction' && key !== 'nulls')
343
+ return false;
344
+ }
345
+ return true;
346
+ }
272
347
  /**
273
348
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
274
349
  * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
package/dist/client.d.ts CHANGED
@@ -68,6 +68,19 @@ export interface PgCompatPoolClient {
68
68
  * sequential.
69
69
  */
70
70
  readonly supportsPipelining?: boolean;
71
+ /**
72
+ * Optional engine seam: scope a transaction's user callback to its own
73
+ * async subtree. When present, `TurbineClient.transaction` / `$transaction`
74
+ * invoke the callback as `wrapTransactionCallback(() => fn(tx))` instead of
75
+ * `fn(tx)` directly. Single-writer engines (PowDB) implement it with
76
+ * `AsyncLocalStorage.run()` to plant their re-entrancy marker so that it
77
+ * exists ONLY inside the callback's async subtree: a transaction opened
78
+ * from inside the callback is detected as re-entrant (typed E017), while
79
+ * the CALLER's context stays unmarked, so same-tick sibling transactions
80
+ * queue FIFO instead of being falsely flagged. Absent on pg and every other
81
+ * engine, in which case the callback runs unwrapped (zero behavior change).
82
+ */
83
+ wrapTransactionCallback?<R>(fn: () => Promise<R>): Promise<R>;
71
84
  }
72
85
  /**
73
86
  * Minimal pg-compatible pool. Pass any driver that satisfies this interface
package/dist/client.js CHANGED
@@ -775,7 +775,12 @@ export class TurbineClient {
775
775
  try {
776
776
  await client.query(this.dialect.beginStatement());
777
777
  began = true;
778
- const result = await fn(client);
778
+ // Engine seam: single-writer engines scope their transaction re-entrancy
779
+ // marker to the callback's async subtree (see
780
+ // PgCompatPoolClient.wrapTransactionCallback). Absent everywhere else.
781
+ const wrap = client.wrapTransactionCallback;
782
+ // `.call` erases the generic, so the callback's Promise<T> is re-asserted.
783
+ const result = wrap ? (await wrap.call(client, () => fn(client))) : await fn(client);
779
784
  await client.query(this.dialect.commitStatement());
780
785
  return result;
781
786
  }
@@ -868,6 +873,12 @@ export class TurbineClient {
868
873
  }
869
874
  }
870
875
  let result;
876
+ // Engine seam: when the checked-out connection exposes
877
+ // wrapTransactionCallback (single-writer engines such as PowDB), run the user
878
+ // callback through it so the engine can scope its re-entrancy marker to
879
+ // the callback's async subtree. All other drivers: plain fn(tx).
880
+ const wrap = client.wrapTransactionCallback;
881
+ const runCallback = () => (wrap ? wrap.call(client, () => fn(tx)) : fn(tx));
871
882
  if (timeout) {
872
883
  // Race between the function and a timeout. If the timeout fires we
873
884
  // need to actually abort the in-flight query — otherwise the backend
@@ -889,14 +900,14 @@ export class TurbineClient {
889
900
  }, timeout);
890
901
  });
891
902
  try {
892
- result = await Promise.race([fn(tx), timeoutPromise]);
903
+ result = await Promise.race([runCallback(), timeoutPromise]);
893
904
  }
894
905
  finally {
895
906
  clearTimeout(timer);
896
907
  }
897
908
  }
898
909
  else {
899
- result = await fn(tx);
910
+ result = await runCallback();
900
911
  }
901
912
  await client.query(this.dialect.commitStatement());
902
913
  if (this.logging) {
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
43
43
  export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
44
44
  export type { ObserveConfig, ObserveHandle } from './observe.js';
45
45
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
46
- export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
46
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
47
47
  export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
48
48
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
49
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
package/dist/mssql.js CHANGED
@@ -745,11 +745,32 @@ function buildForJsonSubquery(dialect, ctx) {
745
745
  if (hasOrder) {
746
746
  const orderBy = orderEntries
747
747
  .map(([k, dir]) => {
748
- const col = camelToSnake(k);
748
+ // FOR JSON nested ordering supports plain directions and { sort }
749
+ // specs only. Object shapes the core builder compiles with params
750
+ // (JSON-path / vector / relation ordering) must throw here: the
751
+ // shared param-collect mirror is gated on the native path, so a
752
+ // silently-ignored object would desync SQL text from params.
753
+ let rawDir = dir;
754
+ if (typeof dir === 'object' && dir !== null) {
755
+ const sortValue = dir.sort;
756
+ if (typeof sortValue !== 'string') {
757
+ throw new ValidationError(`[turbine] Nested orderBy on "${k}" (table "${targetTable}"): only plain directions and ` +
758
+ `{ sort } specs are supported inside a relation orderBy on SQL Server.`);
759
+ }
760
+ if (dir.nulls !== undefined) {
761
+ throw new UnsupportedFeatureError('NULLS FIRST/LAST ordering', 'sqlserver', 'Explicit nulls placement in orderBy is only available on PostgreSQL and SQLite.');
762
+ }
763
+ rawDir = sortValue;
764
+ }
765
+ // columnMap-first resolution (camelToSnake fallback): matches the
766
+ // core builder's nested orderBy path so camelCase-named DB columns
767
+ // resolve on SQL Server too.
768
+ const col = targetMeta.columnMap[k] ?? camelToSnake(k);
749
769
  if (!targetMeta.allColumns.includes(col)) {
750
- throw new ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
770
+ throw new ValidationError(`[turbine] Unknown field "${k}" in orderBy on table "${targetTable}". ` +
771
+ `Known fields: ${Object.keys(targetMeta.columnMap).join(', ') || '(none)'}.`);
751
772
  }
752
- const safeDir = String(dir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
773
+ const safeDir = String(rawDir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
753
774
  return `${a}.${q(col)} ${safeDir}`;
754
775
  })
755
776
  .join(', ');
package/dist/powdb.d.ts CHANGED
@@ -120,6 +120,18 @@ interface PowdbClientPool {
120
120
  withClient<T>(fn: (c: PowdbClient) => Promise<T>): Promise<T>;
121
121
  close(): Promise<void>;
122
122
  }
123
+ interface PowdbModule {
124
+ Client: {
125
+ connect(opts: PowdbConnOptions): Promise<PowdbClient>;
126
+ };
127
+ Pool: new (opts: PowdbConnOptions & {
128
+ max?: number;
129
+ }) => PowdbClientPool;
130
+ isPowDBError?(err: unknown): err is {
131
+ code: string;
132
+ message: string;
133
+ };
134
+ }
123
135
  /** Connection options for {@link turbinePowDB} — host/port, not a connection string. */
124
136
  export interface PowdbConnOptions {
125
137
  host: string;
@@ -260,8 +272,22 @@ export declare class PowdbPool implements PgCompatPool {
260
272
  private readonly txGate;
261
273
  /** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
262
274
  private poolHold;
275
+ /**
276
+ * Clients currently checked out via {@link connect}. The driver pool's
277
+ * `close()` only closes IDLE clients (checked-out ones are documented as the
278
+ * caller's responsibility), so {@link end} destroys these explicitly;
279
+ * otherwise a `disconnect()` racing an unreleased connection would leave a
280
+ * live socket holding the process open until the server's idle timeout.
281
+ */
282
+ private readonly checkedOut;
263
283
  constructor(pool: PowdbClientPool, toParam?: (v: unknown, i: number) => PowdbParam, options?: PowdbPoolOptions);
264
284
  query(text: QueryArg, values?: unknown[]): Promise<any>;
285
+ /**
286
+ * Typed guard mirroring {@link PowdbEmbeddedPool}: after `end()` the driver
287
+ * pool throws a raw `Error('pool closed')` that {@link wrapPowdbError}
288
+ * cannot classify — surface the same ConnectionError on both transports.
289
+ */
290
+ private assertOpen;
265
291
  connect(): Promise<PgCompatPoolClient>;
266
292
  end(): Promise<void>;
267
293
  }
@@ -361,6 +387,13 @@ export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'de
361
387
  * E017 — queueing that shape would deadlock.
362
388
  */
363
389
  transactionQueueTimeoutMs?: number;
390
+ /**
391
+ * Driver-module injection for the networked target forms (URL / host+port):
392
+ * bypasses the dynamic `import('@zvndev/powdb-client')` and uses this object
393
+ * as the driver module instead. Intended for tests (a fake pool that counts
394
+ * connections) and advanced embedding; everyday callers never set it.
395
+ */
396
+ powdbClientModule?: PowdbModule;
364
397
  }
365
398
  /**
366
399
  * Selects the **embedded** transport — an in-process `@zvndev/powdb-embedded`