turbine-orm 0.60.1 → 0.62.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 (84) hide show
  1. package/README.md +71 -27
  2. package/dist/cjs/cli/config.d.ts +40 -0
  3. package/dist/cjs/cli/config.js +74 -2
  4. package/dist/cjs/cli/index.d.ts +85 -1
  5. package/dist/cjs/cli/index.js +374 -24
  6. package/dist/cjs/cli/mcp.d.ts +8 -0
  7. package/dist/cjs/cli/mcp.js +448 -29
  8. package/dist/cjs/cli/pii-tags.d.ts +64 -9
  9. package/dist/cjs/cli/pii-tags.js +218 -39
  10. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  11. package/dist/cjs/cli/studio.d.ts +23 -0
  12. package/dist/cjs/cli/studio.js +126 -53
  13. package/dist/cjs/cli/ui.d.ts +15 -1
  14. package/dist/cjs/cli/ui.js +19 -5
  15. package/dist/cjs/client.js +248 -11
  16. package/dist/cjs/errors.d.ts +38 -1
  17. package/dist/cjs/errors.js +235 -24
  18. package/dist/cjs/index.d.ts +2 -2
  19. package/dist/cjs/index.js +7 -2
  20. package/dist/cjs/pipeline-submittable.js +26 -3
  21. package/dist/cjs/pipeline.js +15 -2
  22. package/dist/cjs/powql.d.ts +12 -0
  23. package/dist/cjs/powql.js +46 -21
  24. package/dist/cjs/prisma-compat.d.ts +15 -5
  25. package/dist/cjs/prisma-compat.js +273 -78
  26. package/dist/cjs/query/aggregates.d.ts +1 -1
  27. package/dist/cjs/query/aggregates.js +24 -10
  28. package/dist/cjs/query/batched-loader.d.ts +9 -4
  29. package/dist/cjs/query/batched-loader.js +4 -1
  30. package/dist/cjs/query/builder.d.ts +47 -0
  31. package/dist/cjs/query/builder.js +149 -21
  32. package/dist/cjs/query/index.d.ts +3 -1
  33. package/dist/cjs/query/index.js +7 -1
  34. package/dist/cjs/query/option-surface.d.ts +11 -0
  35. package/dist/cjs/query/option-surface.js +13 -0
  36. package/dist/cjs/query/relations.d.ts +8 -0
  37. package/dist/cjs/query/relations.js +21 -1
  38. package/dist/cjs/query/types.d.ts +152 -18
  39. package/dist/cjs/query/types.js +212 -1
  40. package/dist/cjs/query/where.d.ts +3 -3
  41. package/dist/cjs/query/where.js +8 -2
  42. package/dist/cjs/query/writes.js +10 -9
  43. package/dist/cli/config.d.ts +40 -0
  44. package/dist/cli/config.js +73 -2
  45. package/dist/cli/index.d.ts +85 -1
  46. package/dist/cli/index.js +373 -27
  47. package/dist/cli/mcp.d.ts +8 -0
  48. package/dist/cli/mcp.js +448 -29
  49. package/dist/cli/pii-tags.d.ts +64 -9
  50. package/dist/cli/pii-tags.js +217 -39
  51. package/dist/cli/studio-ui.generated.js +1 -1
  52. package/dist/cli/studio.d.ts +23 -0
  53. package/dist/cli/studio.js +125 -53
  54. package/dist/cli/ui.d.ts +15 -1
  55. package/dist/cli/ui.js +18 -4
  56. package/dist/client.js +250 -13
  57. package/dist/errors.d.ts +38 -1
  58. package/dist/errors.js +234 -23
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +5 -2
  61. package/dist/pipeline-submittable.js +26 -3
  62. package/dist/pipeline.js +15 -2
  63. package/dist/powql.d.ts +12 -0
  64. package/dist/powql.js +46 -21
  65. package/dist/prisma-compat.d.ts +15 -5
  66. package/dist/prisma-compat.js +274 -79
  67. package/dist/query/aggregates.d.ts +1 -1
  68. package/dist/query/aggregates.js +24 -10
  69. package/dist/query/batched-loader.d.ts +9 -4
  70. package/dist/query/batched-loader.js +4 -1
  71. package/dist/query/builder.d.ts +47 -0
  72. package/dist/query/builder.js +148 -21
  73. package/dist/query/index.d.ts +3 -1
  74. package/dist/query/index.js +2 -0
  75. package/dist/query/option-surface.d.ts +11 -0
  76. package/dist/query/option-surface.js +13 -0
  77. package/dist/query/relations.d.ts +8 -0
  78. package/dist/query/relations.js +21 -1
  79. package/dist/query/types.d.ts +152 -18
  80. package/dist/query/types.js +207 -2
  81. package/dist/query/where.d.ts +3 -3
  82. package/dist/query/where.js +8 -2
  83. package/dist/query/writes.js +10 -9
  84. package/package.json +13 -3
package/dist/powql.js CHANGED
@@ -41,6 +41,10 @@ import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetim
41
41
  import { assertAggregatePiiOptIn } from './query/aggregates.js';
42
42
  import { expandCompoundUniqueWhere } from './query/compound-unique.js';
43
43
  import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
44
+ // The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
45
+ // are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
46
+ // engines, so a spread request body cannot turn either on here either.
47
+ import { assertDirectionToken, resolveUnsafeFlag, UNSAFE } from './query/types.js';
44
48
  import { escapeLike } from './query/utils.js';
45
49
  import { assertJsonFilterKeys, jsonStringEntries } from './query/where.js';
46
50
  import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
@@ -954,6 +958,21 @@ export class PowqlInterface {
954
958
  * contract): for identical cross-engine results pass `nulls: 'last'`
955
959
  * explicitly on Postgres, which defaults nulls-first for `desc`.
956
960
  */
961
+ /**
962
+ * Validate one orderBy direction token and return the PowQL keyword.
963
+ *
964
+ * Every direction site on this engine used to be spelled
965
+ * `x === 'desc' ? 'desc' : 'asc'`, so `'DESC'`, a token the CORE explicitly
966
+ * accepts and honours, compiled to `asc` here: identical application code
967
+ * sorted one way on Postgres and silently the opposite way on PowDB. The rule
968
+ * is core's {@link assertDirectionToken}, reused rather than re-derived, so
969
+ * the two engines cannot drift again. `undefined` stays "not specified"
970
+ * (defaults asc); `null` is a VALUE and is refused like any other bad token.
971
+ */
972
+ dirKeyword(value, context) {
973
+ assertDirectionToken(value, context);
974
+ return typeof value === 'string' && value.toLowerCase() === 'desc' ? 'desc' : 'asc';
975
+ }
957
976
  buildOrder(orderBy, params, alias) {
958
977
  if (!orderBy)
959
978
  return '';
@@ -974,7 +993,7 @@ export class PowqlInterface {
974
993
  if (spec.nulls === 'first') {
975
994
  throw new UnsupportedFeatureError('NULLS FIRST placement', 'PowDB', `field "${field}": PowDB orders NULLs / missing keys LAST in both directions`);
976
995
  }
977
- return `${this.ref(field, alias)} ${spec.sort === 'desc' ? 'desc' : 'asc'}`;
996
+ return `${this.ref(field, alias)} ${this.dirKeyword(spec.sort, `orderBy on "${field}"`)}`;
978
997
  }
979
998
  // Name the actual feature in the refusal, a pick-row ordering
980
999
  // reported as "vector / distance ordering" sends users hunting for
@@ -990,7 +1009,7 @@ export class PowqlInterface {
990
1009
  : 'object-valued ordering';
991
1010
  throw new UnsupportedFeatureError(feature, 'PowDB', `field "${field}"`);
992
1011
  }
993
- return `${this.ref(field, alias)} ${dir === 'desc' ? 'desc' : 'asc'}`;
1012
+ return `${this.ref(field, alias)} ${this.dirKeyword(dir, `orderBy on "${field}"`)}`;
994
1013
  });
995
1014
  return ` order ${parts.join(', ')}`;
996
1015
  }
@@ -1008,7 +1027,7 @@ export class PowqlInterface {
1008
1027
  // `type: 'numeric'` casts a JSON STRING number for numeric ordering; native
1009
1028
  // JSON numbers already order numerically without a cast.
1010
1029
  const expr = spec.type === 'numeric' ? `cast(${pathExpr}, "float")` : pathExpr;
1011
- return `${expr} ${spec.direction === 'desc' ? 'desc' : 'asc'}`;
1030
+ return `${expr} ${this.dirKeyword(spec.direction, `JSON-path orderBy on "${field}"`)}`;
1012
1031
  }
1013
1032
  // -------------------------------------------------------------------------
1014
1033
  // Execution plumbing
@@ -1153,7 +1172,7 @@ export class PowqlInterface {
1153
1172
  if (linkPlans.length)
1154
1173
  this.attachLinkRows(entities, linkPlans, native);
1155
1174
  if (residualWith) {
1156
- await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, args.includePii === true);
1175
+ await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, resolveUnsafeFlag(args.includePii, 'includePii'));
1157
1176
  }
1158
1177
  return entities;
1159
1178
  });
@@ -1176,7 +1195,7 @@ export class PowqlInterface {
1176
1195
  throw new UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
1177
1196
  }
1178
1197
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1179
- const cols = this.projectedColumns(args.select, args.omit, args.includePii === true);
1198
+ const cols = this.projectedColumns(args.select, args.omit, resolveUnsafeFlag(args.includePii, 'includePii'));
1180
1199
  // Partition the `with` clause: nested-projection blocks vs loader residue.
1181
1200
  // A parent `distinct` never nests (distinct over a row containing a JSON
1182
1201
  // array is not a defined comparison), and a relation whose field name
@@ -1192,7 +1211,9 @@ export class PowqlInterface {
1192
1211
  if (!opt)
1193
1212
  continue;
1194
1213
  const rel = this.meta.relations[relName];
1195
- const plan = rel && !cols.includes(relName) ? this.planNestedRelation(relName, rel, opt, args.includePii === true) : null;
1214
+ const plan = rel && !cols.includes(relName)
1215
+ ? this.planNestedRelation(relName, rel, opt, resolveUnsafeFlag(args.includePii, 'includePii'))
1216
+ : null;
1196
1217
  if (plan) {
1197
1218
  nestedPlans.push(plan);
1198
1219
  continue;
@@ -1202,7 +1223,7 @@ export class PowqlInterface {
1202
1223
  // inside, only when a genuine candidate reaches it), else it stays on the
1203
1224
  // loaders.
1204
1225
  const linkPlan = rel && !cols.includes(relName)
1205
- ? await this.planLinkPathRelation(relName, rel, opt, args.includePii === true, cols, linkPlans.length + 1)
1226
+ ? await this.planLinkPathRelation(relName, rel, opt, resolveUnsafeFlag(args.includePii, 'includePii'), cols, linkPlans.length + 1)
1206
1227
  : null;
1207
1228
  if (linkPlan)
1208
1229
  linkPlans.push(linkPlan);
@@ -1295,7 +1316,7 @@ export class PowqlInterface {
1295
1316
  if (linkPlans.length)
1296
1317
  this.attachLinkRows(entities, linkPlans, native);
1297
1318
  if (residualWith)
1298
- await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, args.includePii === true);
1319
+ await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, resolveUnsafeFlag(args.includePii, 'includePii'));
1299
1320
  return entities[0];
1300
1321
  });
1301
1322
  }
@@ -1311,7 +1332,7 @@ export class PowqlInterface {
1311
1332
  if (linkPlans.length)
1312
1333
  this.attachLinkRows(entities, linkPlans, native);
1313
1334
  if (residualWith)
1314
- await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, args.includePii === true);
1335
+ await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, resolveUnsafeFlag(args.includePii, 'includePii'));
1315
1336
  return entities[0];
1316
1337
  });
1317
1338
  }
@@ -1431,7 +1452,9 @@ export class PowqlInterface {
1431
1452
  where: childWhere,
1432
1453
  with: options.with,
1433
1454
  timeout: options.timeout ?? timeout,
1434
- includePii,
1455
+ // Re-entering the public findMany, so the opt-in has to be spelled
1456
+ // in its PUBLIC form (the sentinel); a plain `true` is refused there.
1457
+ includePii: includePii ? UNSAFE : undefined,
1435
1458
  }));
1436
1459
  for (const child of children) {
1437
1460
  const k = this.joinKey(child[childKeyField]);
@@ -1536,7 +1559,8 @@ export class PowqlInterface {
1536
1559
  where,
1537
1560
  with: options.with,
1538
1561
  timeout: options.timeout ?? timeout,
1539
- includePii,
1562
+ // Public findMany, so the sentinel form. See loadRelation above.
1563
+ includePii: includePii ? UNSAFE : undefined,
1540
1564
  }));
1541
1565
  for (const t of targets)
1542
1566
  targetByPk.set(String(t[targetPkField]), t);
@@ -2293,7 +2317,7 @@ export class PowqlInterface {
2293
2317
  const params = [];
2294
2318
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
2295
2319
  const where = this.buildWhere(resolvedWhere, params);
2296
- this.assertCompiledWhere(where, args.allowFullTableScan, 'updateMany');
2320
+ this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'updateMany');
2297
2321
  const setClause = this.buildUpdateAssignments(args.data, params);
2298
2322
  const filter = where ? ` filter ${where}` : '';
2299
2323
  const { rowCount } = await this.exec(`${this.qt}${filter} update { ${setClause} }`, params, args.timeout, 'updateMany');
@@ -2443,7 +2467,7 @@ export class PowqlInterface {
2443
2467
  const params = [];
2444
2468
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
2445
2469
  const where = this.buildWhere(resolvedWhere, params);
2446
- this.assertCompiledWhere(where, args.allowFullTableScan, 'deleteMany');
2470
+ this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'deleteMany');
2447
2471
  const filter = where ? ` filter ${where}` : '';
2448
2472
  const { rowCount } = await this.exec(`${this.qt}${filter} delete`, params, args.timeout, 'deleteMany');
2449
2473
  return { count: rowCount };
@@ -2582,7 +2606,7 @@ export class PowqlInterface {
2582
2606
  const powfn = fn.slice(1); // sum/avg/min/max
2583
2607
  // Same PII contract as the SQL engines: _min/_max return a stored cell.
2584
2608
  if (fn === '_min' || fn === '_max') {
2585
- assertAggregatePiiOptIn(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, args.includePii);
2609
+ assertAggregatePiiOptIn(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, resolveUnsafeFlag(args.includePii, 'includePii'));
2586
2610
  }
2587
2611
  acc[field] = await scalar(`${powfn}(${this.qt}${filter} { ${this.ref(field)} })`);
2588
2612
  }
@@ -2636,7 +2660,7 @@ export class PowqlInterface {
2636
2660
  for (const entry of args.by) {
2637
2661
  if (typeof entry === 'string') {
2638
2662
  const col = this.column(entry);
2639
- assertAggregatePiiOptIn(this.table, this.meta, entry, col.name, 'groupBy `by` key', args.includePii);
2663
+ assertAggregatePiiOptIn(this.table, this.meta, entry, col.name, 'groupBy `by` key', resolveUnsafeFlag(args.includePii, 'includePii'));
2640
2664
  claim(entry, `column "${col.name}"`);
2641
2665
  if (col.name !== entry)
2642
2666
  claim(col.name, `column "${col.name}"`);
@@ -2650,7 +2674,7 @@ export class PowqlInterface {
2650
2674
  if (!isJsonColumn(col)) {
2651
2675
  throw new ValidationError(`[turbine] groupBy JSON group key on "${entry.field}" (table "${this.table}") requires a json column.`);
2652
2676
  }
2653
- assertAggregatePiiOptIn(this.table, this.meta, entry.field, col.name, 'groupBy JSON `by` key', args.includePii);
2677
+ assertAggregatePiiOptIn(this.table, this.meta, entry.field, col.name, 'groupBy JSON `by` key', resolveUnsafeFlag(args.includePii, 'includePii'));
2654
2678
  this.assertJsonPath('group key', entry.field, entry.path);
2655
2679
  const pathExpr = this.jsonPathExpr(col, entry.path, params);
2656
2680
  const alias = entry.alias ?? String(entry.path[entry.path.length - 1]);
@@ -2701,7 +2725,7 @@ export class PowqlInterface {
2701
2725
  if (target === true) {
2702
2726
  const col = this.column(key);
2703
2727
  if (fn === '_min' || fn === '_max') {
2704
- assertAggregatePiiOptIn(this.table, this.meta, key, col.name, `groupBy ${fn}`, args.includePii);
2728
+ assertAggregatePiiOptIn(this.table, this.meta, key, col.name, `groupBy ${fn}`, resolveUnsafeFlag(args.includePii, 'includePii'));
2705
2729
  }
2706
2730
  claim(`${fn}_${col.name}`, `${fn} of column "${col.name}"`);
2707
2731
  const inner = `.${col.name}`;
@@ -2716,7 +2740,7 @@ export class PowqlInterface {
2716
2740
  throw new ValidationError(`[turbine] groupBy ${fn} target "${key}" on "${target.field}" (table "${this.table}") requires a json column.`);
2717
2741
  }
2718
2742
  if (fn === '_min' || fn === '_max') {
2719
- assertAggregatePiiOptIn(this.table, this.meta, target.field, col.name, `groupBy ${fn} JSON target`, args.includePii);
2743
+ assertAggregatePiiOptIn(this.table, this.meta, target.field, col.name, `groupBy ${fn} JSON target`, resolveUnsafeFlag(args.includePii, 'includePii'));
2720
2744
  }
2721
2745
  this.assertJsonPath(`${fn} target "${key}"`, target.field, target.path);
2722
2746
  const alwaysNumeric = fn === '_sum' || fn === '_avg';
@@ -2916,9 +2940,9 @@ export class PowqlInterface {
2916
2940
  if (spec.nulls === 'first') {
2917
2941
  throw new UnsupportedFeatureError('NULLS FIRST placement', 'PowDB', `groupBy orderBy "${keyForMsg}": PowDB orders NULLs / missing keys LAST in both directions`);
2918
2942
  }
2919
- return spec.sort === 'desc' ? 'desc' : 'asc';
2943
+ return this.dirKeyword(spec.sort, `groupBy orderBy "${keyForMsg}"`);
2920
2944
  }
2921
- return value === 'desc' ? 'desc' : 'asc';
2945
+ return this.dirKeyword(value, `groupBy orderBy "${keyForMsg}"`);
2922
2946
  }
2923
2947
  // -------------------------------------------------------------------------
2924
2948
  // Streaming / unsupported
@@ -2955,7 +2979,8 @@ export class PowqlInterface {
2955
2979
  if (compiledWhere.length > 0)
2956
2980
  return;
2957
2981
  throw new ValidationError(`[turbine] ${action} on "${this.table}" refused: the \`where\` clause is empty. ` +
2958
- `Pass \`allowFullTableScan: true\` to opt in, or check that your filter values are defined.`);
2982
+ "Pass `allowFullTableScan: UNSAFE` to opt in (import { UNSAFE } from 'turbine-orm'), " +
2983
+ 'or check that your filter values are defined.');
2959
2984
  }
2960
2985
  }
2961
2986
  /** Coerce a group-key scalar string by the column's TS type (numbers/bools). */
@@ -40,6 +40,11 @@
40
40
  * by this layer, and a key that is neither a Prisma arg nor a turbine option
41
41
  * gets a dev-mode warning instead of vanishing (see
42
42
  * {@link PRISMA_ARG_KEYS} and `warnUnknownQueryOptions`).
43
+ * The three PRIVILEGE options among them (`skipGlobalFilters`, `includePii`,
44
+ * `allowFullTableScan`) are forwarded VERBATIM, which is what makes them safe
45
+ * here: core accepts only the `UNSAFE` sentinel, so a Prisma-shaped args
46
+ * object carrying `includePii: true` reaches core and is refused there, and
47
+ * this layer never needs its own copy of that rule.
43
48
  *
44
49
  * ## What it deliberately does NOT do (documented divergences)
45
50
  *
@@ -75,11 +80,16 @@
75
80
  * - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
76
81
  * no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
77
82
  * would turn "change at most 10 rows" into "change every matching row".
78
- * - **Write projections** (`select` / `include` / `omit` on
79
- * create/update/delete/upsert), **`select` on `count`**, and
80
- * **`orderBy` / `cursor` / `take` / `skip` on `aggregate`** are accepted and
81
- * IGNORED (they are legitimate Prisma, so they never warn); the full row / a
82
- * plain number comes back.
83
+ * - **Write projections**: `select` / `omit` on create/update/delete/upsert
84
+ * narrow the RETURNED OBJECT (see {@link resolveWriteProjection}); they do
85
+ * NOT narrow the statement's `RETURNING` list, so the column still crosses
86
+ * the wire, and `pii: true` remains the SQL-level control. A relation key in
87
+ * either (or any `include` on a write) THROWS, since a write returns its own
88
+ * row only. `omit` on READS is translated to core's own `omit`, see
89
+ * {@link translateOmit}.
90
+ * - **`select` on `count`** and **`orderBy` / `cursor` / `take` / `skip` on
91
+ * `aggregate`** are accepted and IGNORED (they are legitimate Prisma, so they
92
+ * never warn); a plain number / the aggregate comes back.
83
93
  *
84
94
  * ## Type dependencies (0.41.0)
85
95
  *