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.
Files changed (78) hide show
  1. package/README.md +48 -7
  2. package/dist/cjs/cli/compile-query.d.ts +22 -2
  3. package/dist/cjs/cli/compile-query.js +49 -5
  4. package/dist/cjs/cli/config.d.ts +2 -0
  5. package/dist/cjs/cli/config.js +1 -1
  6. package/dist/cjs/cli/destructive.js +78 -43
  7. package/dist/cjs/cli/index.d.ts +95 -1
  8. package/dist/cjs/cli/index.js +609 -145
  9. package/dist/cjs/cli/mcp.js +30 -1
  10. package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
  11. package/dist/cjs/cli/pii-predicate-guard.js +72 -12
  12. package/dist/cjs/cli/rate-limit.js +38 -1
  13. package/dist/cjs/cli/studio.js +26 -5
  14. package/dist/cjs/cli/ui.d.ts +33 -0
  15. package/dist/cjs/cli/ui.js +53 -7
  16. package/dist/cjs/client.d.ts +13 -1
  17. package/dist/cjs/client.js +1 -1
  18. package/dist/cjs/errors.d.ts +12 -1
  19. package/dist/cjs/errors.js +11 -2
  20. package/dist/cjs/generate.d.ts +26 -0
  21. package/dist/cjs/generate.js +174 -27
  22. package/dist/cjs/index.d.ts +1 -1
  23. package/dist/cjs/index.js +1 -1
  24. package/dist/cjs/introspect.d.ts +17 -0
  25. package/dist/cjs/introspect.js +100 -1
  26. package/dist/cjs/mssql.d.ts +18 -0
  27. package/dist/cjs/mssql.js +20 -1
  28. package/dist/cjs/pipeline.js +44 -6
  29. package/dist/cjs/powql.js +51 -17
  30. package/dist/cjs/query/batched-loader.js +3 -3
  31. package/dist/cjs/query/builder.js +1 -1
  32. package/dist/cjs/query/relations.d.ts +5 -0
  33. package/dist/cjs/query/relations.js +141 -69
  34. package/dist/cjs/query/utils.d.ts +13 -0
  35. package/dist/cjs/query/utils.js +16 -0
  36. package/dist/cjs/serverless.d.ts +1 -1
  37. package/dist/cjs/serverless.js +1 -1
  38. package/dist/cjs/sqlite.d.ts +33 -1
  39. package/dist/cjs/sqlite.js +84 -3
  40. package/dist/cli/compile-query.d.ts +22 -2
  41. package/dist/cli/compile-query.js +50 -6
  42. package/dist/cli/config.d.ts +2 -0
  43. package/dist/cli/config.js +1 -1
  44. package/dist/cli/destructive.js +78 -43
  45. package/dist/cli/index.d.ts +95 -1
  46. package/dist/cli/index.js +604 -147
  47. package/dist/cli/mcp.js +30 -1
  48. package/dist/cli/pii-predicate-guard.d.ts +25 -0
  49. package/dist/cli/pii-predicate-guard.js +73 -13
  50. package/dist/cli/rate-limit.js +38 -1
  51. package/dist/cli/studio.js +27 -6
  52. package/dist/cli/ui.d.ts +33 -0
  53. package/dist/cli/ui.js +51 -7
  54. package/dist/client.d.ts +13 -1
  55. package/dist/client.js +1 -1
  56. package/dist/errors.d.ts +12 -1
  57. package/dist/errors.js +11 -2
  58. package/dist/generate.d.ts +26 -0
  59. package/dist/generate.js +172 -27
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/introspect.d.ts +17 -0
  63. package/dist/introspect.js +98 -1
  64. package/dist/mssql.d.ts +18 -0
  65. package/dist/mssql.js +20 -1
  66. package/dist/pipeline.js +44 -6
  67. package/dist/powql.js +53 -19
  68. package/dist/query/batched-loader.js +4 -4
  69. package/dist/query/builder.js +2 -2
  70. package/dist/query/relations.d.ts +5 -0
  71. package/dist/query/relations.js +141 -70
  72. package/dist/query/utils.d.ts +13 -0
  73. package/dist/query/utils.js +15 -0
  74. package/dist/serverless.d.ts +1 -1
  75. package/dist/serverless.js +1 -1
  76. package/dist/sqlite.d.ts +33 -1
  77. package/dist/sqlite.js +85 -4
  78. package/package.json +2 -2
@@ -22,6 +22,7 @@
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.executePipeline = executePipeline;
24
24
  exports.pipelineSupported = pipelineSupported;
25
+ const dialect_js_1 = require("./dialect.js");
25
26
  const errors_js_1 = require("./errors.js");
26
27
  const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
27
28
  /**
@@ -32,7 +33,7 @@ const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
32
33
  * The caller is responsible for acquiring the client and releasing it after
33
34
  * this function completes (in the finally block).
34
35
  */
35
- async function runSequential(client, queries, options = {}) {
36
+ async function runSequential(client, queries, dialect, options = {}) {
36
37
  const { transactional = true } = options;
37
38
  if (!transactional)
38
39
  return runIndependent(client, queries);
@@ -44,7 +45,7 @@ async function runSequential(client, queries, options = {}) {
44
45
  // `TURBINE_E004` nor a retryable flag for the one failure that is most
45
46
  // worth retrying.
46
47
  try {
47
- await client.query('BEGIN');
48
+ await client.query(dialect.beginStatement());
48
49
  }
49
50
  catch (err) {
50
51
  throw (0, errors_js_1.wrapPgError)(err);
@@ -61,7 +62,7 @@ async function runSequential(client, queries, options = {}) {
61
62
  results.push(q.transform(raw));
62
63
  }
63
64
  try {
64
- await client.query('COMMIT');
65
+ await client.query(dialect.commitStatement());
65
66
  }
66
67
  catch (err) {
67
68
  throw (0, errors_js_1.wrapPgError)(err);
@@ -70,7 +71,7 @@ async function runSequential(client, queries, options = {}) {
70
71
  }
71
72
  catch (err) {
72
73
  try {
73
- await client.query('ROLLBACK');
74
+ await client.query(dialect.rollbackStatement());
74
75
  }
75
76
  catch {
76
77
  // Best-effort rollback
@@ -78,6 +79,40 @@ async function runSequential(client, queries, options = {}) {
78
79
  throw err;
79
80
  }
80
81
  }
82
+ /**
83
+ * The dialect whose transaction keywords this batch must use.
84
+ *
85
+ * `executePipeline` is handed a POOL and nothing else, so the pool is the only
86
+ * place the engine can be read from; the engine pool shims are the objects that
87
+ * know their own dialect, and one of them (`MssqlPool`) publishes it for exactly
88
+ * this. Anything else, a real `pg.Pool`, a serverless HTTP pool, a test mock,
89
+ * keeps PostgreSQL, which is what every one of them already spoke.
90
+ *
91
+ * This existed as three hard-coded strings, `BEGIN` / `COMMIT` / `ROLLBACK`,
92
+ * which is the one engine-specific decision in this file and the one it was not
93
+ * making. On SQL Server a bare `BEGIN` opens a statement BLOCK, not a
94
+ * transaction: `MssqlTxClient` matches the dialect's `BEGIN TRANSACTION` and
95
+ * nothing else, so a bare `BEGIN` missed that branch, reached the server as a
96
+ * block opener with no `END`, and was rejected; the `COMMIT` and `ROLLBACK`
97
+ * that followed then found no open transaction and silently did nothing. A
98
+ * pipeline that documents itself as atomic was neither atomic nor rolled back.
99
+ *
100
+ * Duck-typed rather than `instanceof`, deliberately: importing an engine module
101
+ * here would pull an optional peer's whole module graph into the Postgres path.
102
+ * The three methods tested are exactly the three called below, so a partial
103
+ * object can never be accepted and then fail at the call site.
104
+ */
105
+ function poolDialect(pool) {
106
+ const candidate = pool.dialect;
107
+ if (candidate !== null &&
108
+ typeof candidate === 'object' &&
109
+ typeof candidate.beginStatement === 'function' &&
110
+ typeof candidate.commitStatement === 'function' &&
111
+ typeof candidate.rollbackStatement === 'function') {
112
+ return candidate;
113
+ }
114
+ return dialect_js_1.postgresDialect;
115
+ }
81
116
  /**
82
117
  * Sequential fallback for `{ transactional: false }`: each query is
83
118
  * INDEPENDENT, which is what that option promises and what the real pipeline
@@ -181,8 +216,11 @@ async function executePipeline(pool, queries, options) {
181
216
  const results = await (0, pipeline_submittable_js_1.runPipelined)(client, queries, pipelineOptions);
182
217
  return results;
183
218
  }
184
- // Sequential fallback, reuses the same client
185
- return await runSequential(client, queries, options);
219
+ // Sequential fallback, reuses the same client. This is the path every
220
+ // non-Postgres engine takes (none of their clients expose pg's wire
221
+ // internals), so it is the one that has to speak the engine's transaction
222
+ // keywords rather than Postgres's.
223
+ return await runSequential(client, queries, poolDialect(pool), options);
186
224
  }
187
225
  finally {
188
226
  client.release();
package/dist/cjs/powql.js CHANGED
@@ -185,8 +185,17 @@ function rejectUnsupportedFilter(value, field) {
185
185
  if ('path' in value || 'hasKey' in value) {
186
186
  throw new errors_js_1.UnsupportedFeatureError('JSON path/key filters', 'PowDB', `field "${field}"`);
187
187
  }
188
- if ('hasEvery' in value || 'hasSome' in value || 'isEmpty' in value) {
189
- throw new errors_js_1.UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
188
+ // Enumerated from the SHARED {@link ARRAY_OPERATOR_KEYS} rather than
189
+ // re-listed here, because the hand-written copy had already drifted from it:
190
+ // `has` was missing, so `{ tags: { has: 'x' } }` was not recognised as an
191
+ // array filter at all, fell through to the bare-object equality branch, and
192
+ // bound the whole `{ has: 'x' }` OBJECT as a scalar parameter. A silent wrong
193
+ // answer where its three siblings raise E017. Two lists is how that happened;
194
+ // reading the one the SQL side reads is what stops it happening again.
195
+ for (const key of filters_js_1.ARRAY_OPERATOR_KEYS) {
196
+ if (key in value) {
197
+ throw new errors_js_1.UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
198
+ }
190
199
  }
191
200
  if ('search' in value) {
192
201
  throw new errors_js_1.UnsupportedFeatureError('full-text search filters', 'PowDB', `field "${field}"`);
@@ -216,7 +225,7 @@ class PowqlInterface {
216
225
  this.options = options;
217
226
  const meta = schema.tables[table];
218
227
  if (!meta) {
219
- throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". Available: ${Object.keys(schema.tables).join(', ')}`);
228
+ throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". ${(0, utils_js_1.availableClause)(Object.keys(schema.tables), 'The schema has no tables.')}`);
220
229
  }
221
230
  this.meta = meta;
222
231
  this.defaultLimit = options.defaultLimit;
@@ -1189,8 +1198,18 @@ class PowqlInterface {
1189
1198
  stripWritePii(entity) {
1190
1199
  if (!entity)
1191
1200
  return entity;
1201
+ // Same PRIMARY-KEY exemption as the SQL engines' `piiColumns` / `piiFields`
1202
+ // (query/writes.ts), and for the same measured reason: the returned row has
1203
+ // to stay ADDRESSABLE. Without it a PII-tagged key column came back deleted,
1204
+ // so the caller's follow-up `update`/`delete` built its where from a partial
1205
+ // key, the empty-where guard did not fire (the other member was present),
1206
+ // and the write hit every row sharing the remaining member. The policy is
1207
+ // "tag sensitive data, not keys": a PII PK is documented out of scope for
1208
+ // stripping. This docstring already cross-referenced the SQL rule; only the
1209
+ // exemption itself was missing.
1210
+ const pk = new Set(this.meta.primaryKey);
1192
1211
  for (const col of this.meta.columns) {
1193
- if (col.pii)
1212
+ if (col.pii && !pk.has(col.name))
1194
1213
  delete entity[col.field];
1195
1214
  }
1196
1215
  return entity;
@@ -1458,18 +1477,40 @@ class PowqlInterface {
1458
1477
  if (args.cursor) {
1459
1478
  throw new errors_js_1.UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
1460
1479
  }
1480
+ // `distinct` names COLUMNS: Postgres compiles it to `SELECT DISTINCT ON
1481
+ // (col)`, one row per distinct value of those columns. PowQL's `distinct`
1482
+ // keyword is ROW-WIDE (`SELECT DISTINCT *`) and takes no column list, and
1483
+ // the language has no window functions to rebuild per-column distinct with,
1484
+ // so there is nothing faithful to emit. Accepting the option and emitting
1485
+ // the row-wide keyword returned a DIFFERENT row set under the same
1486
+ // argument, with no error and no warning, which is the one outcome the
1487
+ // dialect seam exists to prevent.
1488
+ //
1489
+ // Refused with the same E017 the other non-Postgres engines already raise
1490
+ // (query/builder.ts's `dialect.name !== 'postgresql'` gate) and in the same
1491
+ // words, so `distinct` now means one thing across every engine and matches
1492
+ // what the README documents. Refused BEFORE the column names are resolved,
1493
+ // matching that gate's position, so `distinct: ['nope']` reports the
1494
+ // unsupported feature rather than the typo on every engine alike.
1495
+ if (args.distinct?.length) {
1496
+ throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', 'PowDB', "findMany({ distinct }) requires PostgreSQL: PowQL's `distinct` is row-wide, not per-column. " +
1497
+ 'Group in application code, or use groupBy({ by }) for one row per combination.');
1498
+ }
1461
1499
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1462
1500
  const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
1463
1501
  // Partition the `with` clause: nested-projection blocks vs loader residue.
1464
- // A parent `distinct` never nests (distinct over a row containing a JSON
1465
- // array is not a defined comparison), and a relation whose field name
1466
- // collides with a projected parent column stays on the loaders (its block
1467
- // key would duplicate the column's).
1502
+ // A relation whose field name collides with a projected parent column stays
1503
+ // on the loaders (its block key would duplicate the column's). A parent
1504
+ // `distinct` used to be a third exclusion here (distinct over a row
1505
+ // containing a JSON array is not a defined comparison); the refusal above
1506
+ // now settles that one statement earlier, so the branch cannot be reached
1507
+ // with a `distinct` present and testing for it again would be dead code
1508
+ // implying the option is still accepted.
1468
1509
  const withClause = args.with;
1469
1510
  const nestedPlans = [];
1470
1511
  const linkPlans = [];
1471
1512
  let residualWith = withClause;
1472
- if (withClause && !args.distinct?.length && this.nestedProjectionsPreferred(args)) {
1513
+ if (withClause && this.nestedProjectionsPreferred(args)) {
1473
1514
  const residue = {};
1474
1515
  for (const [relName, opt] of Object.entries(withClause)) {
1475
1516
  if (!opt)
@@ -1500,13 +1541,6 @@ class PowqlInterface {
1500
1541
  const alias = nest ? 't0' : undefined;
1501
1542
  let where = this.buildWhere(resolvedWhere, params, alias);
1502
1543
  where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
1503
- // PowQL's `distinct` is row-wide, so these names never reach the emitted
1504
- // statement. They are still caller-supplied names, and a name resolves or
1505
- // throws: reading the array for its LENGTH alone let `distinct: ['nope']`
1506
- // succeed here while every SQL engine refuses it. Validation only.
1507
- for (const key of args.distinct ?? [])
1508
- this.column(key);
1509
- const distinct = args.distinct?.length ? ' distinct' : '';
1510
1544
  const filter = where ? ` filter ${where}` : '';
1511
1545
  const order = this.buildOrder(args.orderBy, params, alias);
1512
1546
  const limit = this.effectiveLimit(args);
@@ -1534,7 +1568,7 @@ class PowqlInterface {
1534
1568
  else {
1535
1569
  projection = this.projection(cols);
1536
1570
  }
1537
- const powql = `${this.qt}${nest ? ' as t0' : ''}${distinct}${filter}${order}${limitClause}${offsetClause} ${projection}`;
1571
+ const powql = `${this.qt}${nest ? ' as t0' : ''}${filter}${order}${limitClause}${offsetClause} ${projection}`;
1538
1572
  return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
1539
1573
  }
1540
1574
  /** Build + run the findMany select; returns raw rows, the serving wire, the resolved where, and the `with` partition. */
@@ -429,7 +429,7 @@ function resolveCountRelations(parentMeta, countSpec) {
429
429
  const rel = (0, utils_js_1.ownLookup)(parentMeta.relations, relName);
430
430
  if (!rel) {
431
431
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
432
- `Available: ${Object.keys(parentMeta.relations).join(', ')}`);
432
+ (0, utils_js_1.availableClause)(Object.keys(parentMeta.relations), 'It has no relations.'));
433
433
  }
434
434
  if (!isToMany(rel)) {
435
435
  throw new errors_js_1.ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
@@ -505,7 +505,7 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
505
505
  // E005 for this exact shape (relations.ts), and under 'auto' the two
506
506
  // must refuse identically or the error CODE depends on table size.
507
507
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
508
- `Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}`);
508
+ (0, utils_js_1.availableClause)(Object.keys(ctx.parentMeta.relations), 'It has no relations.'));
509
509
  }
510
510
  resolved.push({ relName, rel, options: spec === true ? {} : spec });
511
511
  }
@@ -533,7 +533,7 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
533
533
  // not a line in a correctness fix. Until then both strategies say no.
534
534
  if (hasCount && depth > 0) {
535
535
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "_count" on table "${ctx.parentMeta.name}". ` +
536
- `Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}. ` +
536
+ `${(0, utils_js_1.availableClause)(Object.keys(ctx.parentMeta.relations), 'It has no relations.')} ` +
537
537
  '(`_count` is supported on the top-level `with` only, on every relationLoadStrategy.)');
538
538
  }
539
539
  // Fix key order BEFORE anything is awaited: the loads below all write their
@@ -707,7 +707,7 @@ class QueryInterface {
707
707
  this.schema = schema;
708
708
  const meta = schema.tables[table];
709
709
  if (!meta) {
710
- throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". Available: ${Object.keys(schema.tables).join(', ')}`);
710
+ throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". ${(0, utils_js_1.availableClause)(Object.keys(schema.tables), 'The schema has no tables.')}`);
711
711
  }
712
712
  this.tableMeta = meta;
713
713
  this.middlewares = middlewares ?? [];
@@ -71,6 +71,11 @@ export declare function resolveProjection(qi: BuilderCtx, table: string, meta: T
71
71
  * the existing call sites in builder.ts.
72
72
  */
73
73
  export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
74
+ /**
75
+ * Read a relation spec as options. See {@link EMPTY_WITH_OPTIONS} for why this
76
+ * is a shared authority rather than a guard repeated per walker.
77
+ */
78
+ export declare function relationOptions(spec: true | WithOptions): WithOptions;
74
79
  /**
75
80
  * Produce a fingerprint for a `with` clause tree. Recursion mirrors
76
81
  * buildSelectWithRelations / buildRelationSubquery.