turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
@@ -51,6 +51,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.includeKeysForBatching = includeKeysForBatching;
52
52
  exports.stripFields = stripFields;
53
53
  exports.neededParentKeyFields = neededParentKeyFields;
54
+ exports.resolveCountRelations = resolveCountRelations;
54
55
  exports.loadRelationsBatched = loadRelationsBatched;
55
56
  const errors_js_1 = require("../errors.js");
56
57
  const schema_js_1 = require("../schema.js");
@@ -120,6 +121,14 @@ function neededParentKeyFields(parentMeta, withClause) {
120
121
  for (const [relName, spec] of Object.entries(withClause)) {
121
122
  if (!spec)
122
123
  continue;
124
+ // `_count` needs each counted relation's parent-side key to stitch counts.
125
+ if (relName === '_count') {
126
+ for (const rel of resolveCountRelations(parentMeta, spec)) {
127
+ for (const col of localKeyColumns(rel))
128
+ fields.add(parentMeta.reverseColumnMap[col] ?? col);
129
+ }
130
+ continue;
131
+ }
123
132
  const rel = parentMeta.relations[relName];
124
133
  if (!rel)
125
134
  continue; // unknown relation — the join path throws; let the loader surface it
@@ -140,6 +149,37 @@ function localKeyColumns(rel) {
140
149
  return (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
141
150
  return (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
142
151
  }
152
+ /**
153
+ * Resolve the set of to-many relations a `_count` spec selects. `true` counts
154
+ * every to-many relation (hasMany + manyToMany) of the table; the record form
155
+ * counts only the enabled names. Shared by the join builder and the batched
156
+ * loader so both count the exact same relations.
157
+ *
158
+ * Errors: E005 ({@link RelationError}) for an unknown relation name, E003
159
+ * ({@link ValidationError}) when a named relation is to-one.
160
+ */
161
+ function resolveCountRelations(parentMeta, countSpec) {
162
+ const isToMany = (r) => r.type === 'hasMany' || r.type === 'manyToMany';
163
+ if (countSpec === true) {
164
+ return Object.values(parentMeta.relations).filter(isToMany);
165
+ }
166
+ const out = [];
167
+ for (const [relName, enabled] of Object.entries(countSpec)) {
168
+ if (!enabled)
169
+ continue;
170
+ const rel = parentMeta.relations[relName];
171
+ if (!rel) {
172
+ throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
173
+ `Available: ${Object.keys(parentMeta.relations).join(', ')}`);
174
+ }
175
+ if (!isToMany(rel)) {
176
+ throw new errors_js_1.ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
177
+ `"${parentMeta.name}" is a to-one relation.`);
178
+ }
179
+ out.push(rel);
180
+ }
181
+ return out;
182
+ }
143
183
  /** Stringified stitch key — robust to number/uuid/bigint type drift across a join. */
144
184
  function keyOf(value) {
145
185
  return String(value);
@@ -161,6 +201,11 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
161
201
  for (const [relName, spec] of Object.entries(withClause)) {
162
202
  if (!spec)
163
203
  continue;
204
+ // Reserved `_count` key — one grouped COUNT(*) follow-up per counted relation.
205
+ if (relName === '_count') {
206
+ loads.push(loadCounts(ctx, parents, spec));
207
+ continue;
208
+ }
164
209
  const rel = ctx.parentMeta.relations[relName];
165
210
  if (!rel) {
166
211
  throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
@@ -215,6 +260,7 @@ async function loadToOneOrMany(ctx, parents, rel, relName, options, timeout, dep
215
260
  select: proj.select,
216
261
  omit: proj.omit,
217
262
  orderBy: options.orderBy,
263
+ skipGlobalFilters: ctx.skipGlobalFilters,
218
264
  });
219
265
  const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
220
266
  return deferred.transform(result);
@@ -313,6 +359,7 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
313
359
  select: proj.select,
314
360
  omit: proj.omit,
315
361
  orderBy: options.orderBy,
362
+ skipGlobalFilters: ctx.skipGlobalFilters,
316
363
  });
317
364
  const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
318
365
  return deferred.transform(result);
@@ -347,6 +394,107 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
347
394
  }
348
395
  stripFields(targetsInOrder, proj.strip);
349
396
  }
397
+ /**
398
+ * Load correlated `_count` values for the counted relations. One grouped
399
+ * follow-up per relation (`SELECT key, COUNT(*) … WHERE key = ANY($1) GROUP BY
400
+ * key`), attached onto each parent's `_count` object (0 when a parent has no
401
+ * matching rows) — byte-identical to the join strategy's `_count` output.
402
+ */
403
+ async function loadCounts(ctx, parents, countSpec) {
404
+ const rels = resolveCountRelations(ctx.parentMeta, countSpec);
405
+ // Initialise every parent's `_count` up-front so the concurrent per-relation
406
+ // loads below (each writing its own key) never race on the object creation.
407
+ for (const parent of parents) {
408
+ if (parent._count === undefined)
409
+ parent._count = {};
410
+ }
411
+ await Promise.all(rels.map((rel) => loadOneCount(ctx, parents, rel)));
412
+ }
413
+ /** One grouped COUNT(*) follow-up for a single to-many relation. */
414
+ async function loadOneCount(ctx, parents, rel) {
415
+ let parentKeyCol;
416
+ let childTable;
417
+ let childKeyCol;
418
+ if (rel.type === 'manyToMany') {
419
+ const through = rel.through;
420
+ if (!through) {
421
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
422
+ }
423
+ const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
424
+ const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
425
+ if (sourceRef.length > 1 || sourceJ.length > 1) {
426
+ throw new errors_js_1.UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key m2m _count`);
427
+ }
428
+ parentKeyCol = sourceRef[0];
429
+ childTable = through.table;
430
+ childKeyCol = sourceJ[0];
431
+ }
432
+ else {
433
+ // hasMany: child FK correlates to the parent reference key.
434
+ const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
435
+ const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
436
+ if (fk.length > 1 || rk.length > 1) {
437
+ throw new errors_js_1.UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key _count`);
438
+ }
439
+ parentKeyCol = rk[0];
440
+ childTable = rel.to;
441
+ childKeyCol = fk[0];
442
+ }
443
+ const parentKeyField = ctx.parentMeta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
444
+ const keys = uniqueKeys(parents, parentKeyField);
445
+ const counts = new Map();
446
+ if (keys.length > 0) {
447
+ const qChild = ctx.quote(childTable);
448
+ const qKey = ctx.quote(childKeyCol);
449
+ const chunks = [];
450
+ for (let i = 0; i < keys.length; i += MAX_RELATION_KEYS)
451
+ chunks.push(keys.slice(i, i + MAX_RELATION_KEYS));
452
+ // Global filter on the counted target, matching the join strategy so the
453
+ // two strategies return identical counts under a filter. hasMany filters
454
+ // the counted table directly; m2m counts junction rows but restricts them
455
+ // to junction rows whose TARGET survives the target table's filter via
456
+ // EXISTS — mirroring buildRelationCountExpr's EXISTS-on-target (which also
457
+ // skips the filter when the junction targetKey arity doesn't match the
458
+ // target PK). Rendered after the $1 key array.
459
+ let gf = null;
460
+ if (ctx.tableGlobalFilter) {
461
+ if (rel.type === 'manyToMany' && rel.through) {
462
+ const targetKeys = (0, schema_js_1.normalizeKeyColumns)(rel.through.targetKey);
463
+ const targetMeta = ctx.schema.tables[rel.to];
464
+ const pk = targetMeta?.primaryKey ?? [];
465
+ if (targetMeta && pk.length > 0 && pk.length === targetKeys.length) {
466
+ const targetGf = ctx.tableGlobalFilter(rel.to, 't', 1);
467
+ if (targetGf) {
468
+ const join = targetKeys.map((jc, i) => `t.${ctx.quote(pk[i])} = ${qChild}.${ctx.quote(jc)}`).join(' AND ');
469
+ gf = {
470
+ clause: `EXISTS (SELECT 1 FROM ${ctx.quote(rel.to)} t WHERE ${join} AND ${targetGf.clause})`,
471
+ params: targetGf.params,
472
+ };
473
+ }
474
+ }
475
+ }
476
+ else if (rel.type !== 'manyToMany') {
477
+ gf = ctx.tableGlobalFilter(childTable, qChild, 1);
478
+ }
479
+ }
480
+ const gfAnd = gf ? ` AND ${gf.clause}` : '';
481
+ const results = await Promise.all(chunks.map((chunk) => {
482
+ const params = [ctx.inClauseParam(chunk), ...(gf ? gf.params : [])];
483
+ const predicate = ctx.buildInClause(`${qChild}.${qKey}`, ctx.paramPlaceholder(1), false);
484
+ const sql = `SELECT ${qChild}.${qKey} AS "k", COUNT(*) AS "c" FROM ${qChild} ` +
485
+ `WHERE ${predicate}${gfAnd} GROUP BY ${qChild}.${qKey}`;
486
+ return ctx.exec(sql, params);
487
+ }));
488
+ for (const { rows } of results) {
489
+ for (const row of rows) {
490
+ counts.set(keyOf(row.k), Number(row.c));
491
+ }
492
+ }
493
+ }
494
+ for (const parent of parents) {
495
+ parent._count[rel.name] = counts.get(keyOf(parent[parentKeyField])) ?? 0;
496
+ }
497
+ }
350
498
  // ---------------------------------------------------------------------------
351
499
  // Small helpers
352
500
  // ---------------------------------------------------------------------------