turbine-orm 0.39.0 → 0.40.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.
- package/README.md +9 -3
- package/dist/cjs/cli/index.js +500 -130
- package/dist/cjs/cli/migrate.js +79 -0
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/powql.js +7 -3
- package/dist/cjs/query/aggregates.js +11 -1
- package/dist/cjs/query/builder.js +35 -8
- package/dist/cjs/query/filters.js +33 -0
- package/dist/cjs/query/relations.js +7 -7
- package/dist/cli/index.d.ts +61 -1
- package/dist/cli/index.js +500 -131
- package/dist/cli/migrate.d.ts +33 -0
- package/dist/cli/migrate.js +78 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/powql.js +8 -4
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +12 -2
- package/dist/query/builder.d.ts +15 -3
- package/dist/query/builder.js +36 -9
- package/dist/query/deferred.d.ts +11 -0
- package/dist/query/filters.d.ts +17 -0
- package/dist/query/filters.js +32 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/relations.js +8 -8
- package/dist/query/types.d.ts +86 -4
- package/package.json +3 -3
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -24,6 +24,7 @@ exports.getPendingMigrations = getPendingMigrations;
|
|
|
24
24
|
exports.listMigrationFiles = listMigrationFiles;
|
|
25
25
|
exports.parseMigrationContent = parseMigrationContent;
|
|
26
26
|
exports.parseMigrationSQL = parseMigrationSQL;
|
|
27
|
+
exports.buildDiffMigrationBody = buildDiffMigrationBody;
|
|
27
28
|
exports.createMigration = createMigration;
|
|
28
29
|
exports.deriveLockId = deriveLockId;
|
|
29
30
|
exports.planMigrationDeploy = planMigrationDeploy;
|
|
@@ -223,6 +224,84 @@ exports.MIGRATION_RECIPES = {
|
|
|
223
224
|
build: buildBackfillRecipe,
|
|
224
225
|
},
|
|
225
226
|
};
|
|
227
|
+
/** Loud file-level banner prepended when a diff migration contains destructive statements. */
|
|
228
|
+
const DESTRUCTIVE_MIGRATION_BANNER = [
|
|
229
|
+
'-- ============================================================',
|
|
230
|
+
'-- WARNING: this migration contains DESTRUCTIVE statement(s).',
|
|
231
|
+
'-- Each one is flagged inline below. `turbine migrate up` refuses',
|
|
232
|
+
'-- destructive statements by default: it asks you to confirm',
|
|
233
|
+
'-- interactively, or you must pass --allow-destructive. Review',
|
|
234
|
+
'-- every flagged statement carefully before running.',
|
|
235
|
+
'-- ============================================================',
|
|
236
|
+
];
|
|
237
|
+
/**
|
|
238
|
+
* Annotate a list of SQL statements: scan each for data-destroying operations
|
|
239
|
+
* (via {@link scanDestructiveSql}) and prefix any offender with loud, commented
|
|
240
|
+
* warnings. Statements are left intact so the existing `migrate up` gate still
|
|
241
|
+
* refuses them by default; the comments just make the danger visible on review.
|
|
242
|
+
*/
|
|
243
|
+
function annotateDiffStatements(statements) {
|
|
244
|
+
const destructive = [];
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const raw of statements) {
|
|
247
|
+
const stmt = raw.trim();
|
|
248
|
+
if (!stmt)
|
|
249
|
+
continue;
|
|
250
|
+
const hits = (0, destructive_js_1.scanDestructiveSql)(stmt);
|
|
251
|
+
if (hits.length > 0) {
|
|
252
|
+
destructive.push(...hits);
|
|
253
|
+
for (const h of hits) {
|
|
254
|
+
out.push(`-- !! DESTRUCTIVE [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
255
|
+
}
|
|
256
|
+
out.push('-- !! Refused by default. Confirm interactively or pass --allow-destructive to run it.');
|
|
257
|
+
}
|
|
258
|
+
out.push(stmt.endsWith(';') ? stmt : `${stmt};`);
|
|
259
|
+
}
|
|
260
|
+
return { text: out.join('\n'), destructive };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Build a migration UP/DOWN body from a `schemaDiff()` result.
|
|
264
|
+
*
|
|
265
|
+
* - UP is the diff's forward statements. DOWN is the diff's reverse statements
|
|
266
|
+
* when derivable, otherwise a clearly-commented "irreversible" placeholder.
|
|
267
|
+
* - Destructive statements in EITHER direction (a lossy `ALTER COLUMN ... TYPE`
|
|
268
|
+
* in UP, a `DROP TABLE`/`DROP COLUMN` reverse in DOWN) are flagged inline and,
|
|
269
|
+
* when any exist, a loud file-level banner is prepended to UP.
|
|
270
|
+
* - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
|
|
271
|
+
* enum value removals) are surfaced as `-- NOTE:` comments in UP.
|
|
272
|
+
*
|
|
273
|
+
* Pure and DB-free, so it is unit-testable from a synthesized diff.
|
|
274
|
+
*/
|
|
275
|
+
function buildDiffMigrationBody(diff) {
|
|
276
|
+
const up = annotateDiffStatements(diff.statements);
|
|
277
|
+
const hasReverse = diff.reverseStatements.length > 0;
|
|
278
|
+
const down = hasReverse
|
|
279
|
+
? annotateDiffStatements(diff.reverseStatements)
|
|
280
|
+
: { text: '', destructive: [] };
|
|
281
|
+
const upParts = [];
|
|
282
|
+
if (up.destructive.length > 0 || down.destructive.length > 0) {
|
|
283
|
+
upParts.push(...DESTRUCTIVE_MIGRATION_BANNER, '');
|
|
284
|
+
}
|
|
285
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
286
|
+
for (const w of diff.warnings)
|
|
287
|
+
upParts.push(`-- NOTE: ${w}`);
|
|
288
|
+
upParts.push('');
|
|
289
|
+
}
|
|
290
|
+
upParts.push(up.text || '-- (no statements: schema already matches the database)');
|
|
291
|
+
const downText = hasReverse
|
|
292
|
+
? down.text
|
|
293
|
+
: [
|
|
294
|
+
'-- irreversible, write manually',
|
|
295
|
+
'-- The diff produced no reversible statements for this change. Write the',
|
|
296
|
+
'-- rollback SQL by hand, or leave this section empty for a one-way migration.',
|
|
297
|
+
].join('\n');
|
|
298
|
+
return {
|
|
299
|
+
up: upParts.join('\n'),
|
|
300
|
+
down: downText,
|
|
301
|
+
destructiveUp: up.destructive,
|
|
302
|
+
destructiveDown: down.destructive,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
226
305
|
// ---------------------------------------------------------------------------
|
|
227
306
|
// Commands
|
|
228
307
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/client.js
CHANGED
|
@@ -374,6 +374,7 @@ class TurbineClient {
|
|
|
374
374
|
globalFilters: config.globalFilters,
|
|
375
375
|
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
376
376
|
sqlCache: config.sqlCache ?? true,
|
|
377
|
+
sqlCacheSize: config.sqlCacheSize,
|
|
377
378
|
dialect: config.dialect,
|
|
378
379
|
// Non-SQL backends (PowDB) inject a factory that builds their own query
|
|
379
380
|
// interface (PowqlInterface) instead of the SQL QueryInterface. SQL engines
|
package/dist/cjs/powql.js
CHANGED
|
@@ -749,7 +749,7 @@ class PowqlInterface {
|
|
|
749
749
|
buildOrder(orderBy, params, alias) {
|
|
750
750
|
if (!orderBy)
|
|
751
751
|
return '';
|
|
752
|
-
const keys =
|
|
752
|
+
const keys = (0, filters_js_1.orderByEntries)(orderBy).filter(([, dir]) => dir !== undefined);
|
|
753
753
|
if (!keys.length)
|
|
754
754
|
return '';
|
|
755
755
|
const parts = keys.map(([field, dir]) => {
|
|
@@ -2204,7 +2204,11 @@ class PowqlInterface {
|
|
|
2204
2204
|
}
|
|
2205
2205
|
const having = this.buildHaving(args.having, params, aggInner);
|
|
2206
2206
|
const order = this.buildGroupOrder(args.orderBy, byOrderExprs, aggOrderExprs);
|
|
2207
|
-
|
|
2207
|
+
// LIMIT / OFFSET over the result groups, applied after ORDER BY (mirrors
|
|
2208
|
+
// the SQL groupBy). offset 0 is a no-op, matching findMany.
|
|
2209
|
+
const limitClause = args.limit !== undefined ? ` limit ${this.param(args.limit, params)}` : '';
|
|
2210
|
+
const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
|
|
2211
|
+
const powql = `${this.qt}${filter} group ${groupExprs.join(', ')}${having}${order}${limitClause}${offsetClause} { ${proj.join(', ')} }`;
|
|
2208
2212
|
const { rows, native: resultNative } = await this.exec(powql, params, args.timeout, 'groupBy');
|
|
2209
2213
|
// Reshape: group keys → user fields (coerced / null-disambiguated),
|
|
2210
2214
|
// aggregates → nested `{ _sum: { field } }`; discriminators are stripped.
|
|
@@ -2310,7 +2314,7 @@ class PowqlInterface {
|
|
|
2310
2314
|
return keys.join(', ') || '(none)';
|
|
2311
2315
|
};
|
|
2312
2316
|
const parts = [];
|
|
2313
|
-
for (const [key, value] of
|
|
2317
|
+
for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
|
|
2314
2318
|
if (value === undefined)
|
|
2315
2319
|
continue;
|
|
2316
2320
|
if (aggBlocks.has(key)) {
|
|
@@ -210,6 +210,16 @@ function buildGroupBy(qi, args) {
|
|
|
210
210
|
if (orderSql)
|
|
211
211
|
sql += ` ORDER BY ${orderSql}`;
|
|
212
212
|
}
|
|
213
|
+
// LIMIT / OFFSET over the result groups, applied AFTER ORDER BY. Routed
|
|
214
|
+
// through the dialect pagination hook (parameterized on PG/SQLite/SQL Server,
|
|
215
|
+
// inlined on MySQL); params append after the WHERE/HAVING/ORDER BY params, so
|
|
216
|
+
// no `$n` renumbering. `offset` without a deterministic `orderBy` yields an
|
|
217
|
+
// arbitrary window (same caveat as findMany).
|
|
218
|
+
if (args.limit !== undefined || args.offset !== undefined) {
|
|
219
|
+
const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params) : undefined;
|
|
220
|
+
const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params) : undefined;
|
|
221
|
+
sql += qi.buildPagination(limitPh, offsetPh, args.orderBy !== undefined);
|
|
222
|
+
}
|
|
213
223
|
return {
|
|
214
224
|
sql,
|
|
215
225
|
params,
|
|
@@ -297,7 +307,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
297
307
|
return keys.join(', ') || '(none)';
|
|
298
308
|
};
|
|
299
309
|
const parts = [];
|
|
300
|
-
for (const [key, value] of
|
|
310
|
+
for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
|
|
301
311
|
if (value === undefined)
|
|
302
312
|
continue;
|
|
303
313
|
// Aggregate ordering blocks.
|
|
@@ -185,8 +185,12 @@ class QueryInterface {
|
|
|
185
185
|
table;
|
|
186
186
|
schema;
|
|
187
187
|
tableMeta;
|
|
188
|
-
/**
|
|
189
|
-
|
|
188
|
+
/**
|
|
189
|
+
* SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name).
|
|
190
|
+
* Capacity is set once in the constructor from `options.sqlCacheSize`
|
|
191
|
+
* (default 1000). See {@link QueryInterfaceOptions.sqlCacheSize}.
|
|
192
|
+
*/
|
|
193
|
+
sqlTemplateCache;
|
|
190
194
|
/**
|
|
191
195
|
* Whether the most recent {@link acquireSql} call was a cache HIT. Read by
|
|
192
196
|
* {@link crossCheckCache} to decide whether to run the dev-mode lockstep
|
|
@@ -314,7 +318,13 @@ class QueryInterface {
|
|
|
314
318
|
: warnOpt !== false;
|
|
315
319
|
this.utcTimestamps = options?.utcTimestamps !== false;
|
|
316
320
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
317
|
-
|
|
321
|
+
// SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
|
|
322
|
+
// (mirrors `sqlCache: false`); any positive integer sets the LRU bound;
|
|
323
|
+
// undefined keeps the historical 1000-entry default. A negative value is
|
|
324
|
+
// treated as the default rather than throwing.
|
|
325
|
+
const sqlCacheSize = options?.sqlCacheSize;
|
|
326
|
+
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
327
|
+
this.sqlTemplateCache = new utils_js_1.LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
318
328
|
this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
319
329
|
this.relationLoadStrategy = options?.relationLoadStrategy ?? 'join';
|
|
320
330
|
this.jsonEncoding = options?.jsonEncoding ?? 'object';
|
|
@@ -1110,7 +1120,7 @@ class QueryInterface {
|
|
|
1110
1120
|
// Checked BEFORE the SQL cache so build and warm-cache paths throw
|
|
1111
1121
|
// identically (same rule as the vector guard inside the distinct branch).
|
|
1112
1122
|
if (args?.distinct && args.distinct.length > 0 && args.orderBy) {
|
|
1113
|
-
for (const d of
|
|
1123
|
+
for (const [, d] of (0, filters_js_1.orderByEntries)(args.orderBy)) {
|
|
1114
1124
|
if (this.isRelationOrderByValue(d)) {
|
|
1115
1125
|
throw new errors_js_1.ValidationError('[turbine] `distinct` cannot be combined with relation orderBy (pick-row, `_count`, or ' +
|
|
1116
1126
|
'to-one relation ordering): the outer re-order cannot reference the parent table.');
|
|
@@ -1129,8 +1139,13 @@ class QueryInterface {
|
|
|
1129
1139
|
// Build fingerprint for cache lookup
|
|
1130
1140
|
const whereFp = hasWhere ? this.fingerprintWhere(whereObj) : '';
|
|
1131
1141
|
const withFp = args?.with ? this.withFingerprint(args.with) : '';
|
|
1142
|
+
// Flatten via orderByEntries so the object form (`{ a, b }`) and the
|
|
1143
|
+
// Prisma-style array form (`[{ a }, { b }]`) fingerprint from the SAME
|
|
1144
|
+
// ordered entry list the build/collect paths consume. An array's element
|
|
1145
|
+
// order is authoritative and preserved here, so a permuted array is a
|
|
1146
|
+
// distinct key (correct, it emits a different ORDER BY).
|
|
1132
1147
|
const orderFp = args?.orderBy
|
|
1133
|
-
?
|
|
1148
|
+
? (0, filters_js_1.orderByEntries)(args.orderBy)
|
|
1134
1149
|
.map(([k, d]) => `${k}:${this.orderByEntryFingerprint(d, (0, utils_js_1.ownLookup)(this.tableMeta.relations, k)?.to)}`)
|
|
1135
1150
|
.join(',')
|
|
1136
1151
|
: '';
|
|
@@ -1199,11 +1214,15 @@ class QueryInterface {
|
|
|
1199
1214
|
// Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
|
|
1200
1215
|
const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
|
|
1201
1216
|
if (cursorEntries.length > 0) {
|
|
1217
|
+
// Resolve the seek direction per cursor field from the flattened
|
|
1218
|
+
// orderBy entries (last wins, matching object-key semantics), so both
|
|
1219
|
+
// the object and array orderBy forms drive the cursor comparison.
|
|
1220
|
+
const orderDirByKey = new Map((0, filters_js_1.orderByEntries)(args.orderBy));
|
|
1202
1221
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
1203
1222
|
const col = this.toSqlColumn(k);
|
|
1204
1223
|
// orderBy values can be the { sort, nulls } spec form: normalize
|
|
1205
1224
|
// before comparing, or a desc spec would seek the ascending side.
|
|
1206
|
-
const dir =
|
|
1225
|
+
const dir = orderDirByKey.get(k);
|
|
1207
1226
|
const desc = (0, filters_js_1.isOrderBySpec)(dir) ? dir.sort === 'desc' : dir === 'desc';
|
|
1208
1227
|
const op = desc ? '<' : '>';
|
|
1209
1228
|
freshParams.push(v);
|
|
@@ -1224,7 +1243,7 @@ class QueryInterface {
|
|
|
1224
1243
|
// need two levels: inner DISTINCT ON ordered by the distinct columns then
|
|
1225
1244
|
// the user's order (picks the right representative row), outer re-ordered
|
|
1226
1245
|
// by the user's order alone.
|
|
1227
|
-
if (
|
|
1246
|
+
if ((0, filters_js_1.orderByEntries)(args.orderBy).some(([, d]) => (0, filters_js_1.isVectorOrderBy)(d))) {
|
|
1228
1247
|
throw new errors_js_1.ValidationError('[turbine] `distinct` cannot be combined with vector distance ordering.');
|
|
1229
1248
|
}
|
|
1230
1249
|
const userOrder = this.buildOrderBy(args.orderBy, freshParams);
|
|
@@ -1638,6 +1657,14 @@ class QueryInterface {
|
|
|
1638
1657
|
// -------------------------------------------------------------------------
|
|
1639
1658
|
// groupBy (with aggregate functions)
|
|
1640
1659
|
// -------------------------------------------------------------------------
|
|
1660
|
+
/**
|
|
1661
|
+
* Group rows and compute per-group aggregates (Prisma-style). The result row
|
|
1662
|
+
* type is INFERRED from the args: each `by` field carries its entity field
|
|
1663
|
+
* type, `_count` is always a number, and each requested `_sum` / `_avg` /
|
|
1664
|
+
* `_min` / `_max` block maps its fields to properly typed values (see
|
|
1665
|
+
* {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
|
|
1666
|
+
* that cannot be typed, so those columns are not projected onto the row type.
|
|
1667
|
+
*/
|
|
1641
1668
|
async groupBy(args) {
|
|
1642
1669
|
return this.executeWithMiddleware('groupBy', args, async () => {
|
|
1643
1670
|
const deferred = this.buildGroupBy(args);
|
|
@@ -1804,7 +1831,7 @@ class QueryInterface {
|
|
|
1804
1831
|
* order exactly so the cached-SQL param re-collection stays in lockstep.
|
|
1805
1832
|
*/
|
|
1806
1833
|
collectOrderByParams(orderBy, params) {
|
|
1807
|
-
for (const [key, dir] of
|
|
1834
|
+
for (const [key, dir] of (0, filters_js_1.orderByEntries)(orderBy)) {
|
|
1808
1835
|
if ((0, filters_js_1.isVectorOrderBy)(dir)) {
|
|
1809
1836
|
const rawColumn = this.toColumn(key);
|
|
1810
1837
|
// Re-run the same validation as buildOrderBy so the collect path can
|
|
@@ -28,6 +28,7 @@ exports.isVectorOrderBy = isVectorOrderBy;
|
|
|
28
28
|
exports.isOrderBySpec = isOrderBySpec;
|
|
29
29
|
exports.isJsonPathOrderBy = isJsonPathOrderBy;
|
|
30
30
|
exports.isRelationPickOrderBy = isRelationPickOrderBy;
|
|
31
|
+
exports.orderByEntries = orderByEntries;
|
|
31
32
|
exports.normalizeOrderBy = normalizeOrderBy;
|
|
32
33
|
const errors_js_1 = require("../errors.js");
|
|
33
34
|
const utils_js_1 = require("./utils.js");
|
|
@@ -356,6 +357,38 @@ function isRelationPickOrderBy(value) {
|
|
|
356
357
|
}
|
|
357
358
|
return true;
|
|
358
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Flatten an orderBy input into an ordered list of `[field, value]` entries.
|
|
362
|
+
*
|
|
363
|
+
* Accepts BOTH the classic single-object form (`{ a: 'asc', b: 'desc' }`,
|
|
364
|
+
* whose insertion order is authoritative) and the Prisma-style array form
|
|
365
|
+
* (`[{ a: 'asc' }, { b: 'desc' }]`, whose array order is authoritative). The
|
|
366
|
+
* array form removes the reliance on JS object key iteration order for
|
|
367
|
+
* multi-key sorts. Each array element may carry one or more keys; they expand
|
|
368
|
+
* left-to-right. `undefined`/non-object elements are skipped.
|
|
369
|
+
*
|
|
370
|
+
* Undefined-VALUED entries are preserved (mirroring `Object.entries`) so each
|
|
371
|
+
* consumer keeps its own `dir !== undefined` filtering exactly as before. This
|
|
372
|
+
* is THE single flattening authority: every ORDER BY compile / collect /
|
|
373
|
+
* fingerprint path routes through it so the array and object forms stay in
|
|
374
|
+
* lockstep across build, param-collect, and cache-key fingerprint.
|
|
375
|
+
*/
|
|
376
|
+
function orderByEntries(orderBy) {
|
|
377
|
+
if (Array.isArray(orderBy)) {
|
|
378
|
+
const entries = [];
|
|
379
|
+
for (const element of orderBy) {
|
|
380
|
+
if (element !== null && typeof element === 'object' && !Array.isArray(element)) {
|
|
381
|
+
for (const kv of Object.entries(element))
|
|
382
|
+
entries.push(kv);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return entries;
|
|
386
|
+
}
|
|
387
|
+
if (orderBy !== null && typeof orderBy === 'object') {
|
|
388
|
+
return Object.entries(orderBy);
|
|
389
|
+
}
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
359
392
|
/**
|
|
360
393
|
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
361
394
|
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
|
@@ -199,7 +199,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
199
199
|
// orderBy shape (OrderBySpec nulls placement changes the SQL, so fingerprint it)
|
|
200
200
|
if (opts.orderBy) {
|
|
201
201
|
const targetRels = qi.schema.tables[relDef.to]?.relations;
|
|
202
|
-
const oEntries =
|
|
202
|
+
const oEntries = (0, filters_js_1.orderByEntries)(opts.orderBy).map(([k, d]) => `${k}:${orderByEntryFingerprint(qi, d, targetRels?.[k]?.to)}`);
|
|
203
203
|
subParts.push(`o=${oEntries.join(',')}`);
|
|
204
204
|
}
|
|
205
205
|
// limit presence, but on inline-pagination engines (MySQL) the literal
|
|
@@ -262,7 +262,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
262
262
|
// orderBy params → where params → limit param → nested-with params
|
|
263
263
|
// (always, both paths).
|
|
264
264
|
if (relDef.type === 'manyToMany') {
|
|
265
|
-
const m2mOrderEntries = spec.orderBy ?
|
|
265
|
+
const m2mOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
266
266
|
if (nativeOrderPath && m2mOrderEntries.length > 0) {
|
|
267
267
|
collectRelationOrderParams(qi, targetTable, targetMeta, m2mOrderEntries, params);
|
|
268
268
|
}
|
|
@@ -284,7 +284,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
287
|
-
const relOrderEntries = spec.orderBy ?
|
|
287
|
+
const relOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
288
288
|
const hasOrder = relOrderEntries.length > 0;
|
|
289
289
|
const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
|
|
290
290
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
@@ -388,7 +388,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
388
388
|
// orderBy keys (object values that are neither a vector nor an OrderBySpec)
|
|
389
389
|
// are validated in the relation branch below, so skip them here.
|
|
390
390
|
if (process.env.NODE_ENV !== 'production') {
|
|
391
|
-
for (const [key, value] of
|
|
391
|
+
for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
|
|
392
392
|
if (isRelationOrderByValue(qi, value) && (0, utils_js_1.ownLookup)(qi.tableMeta.relations, key))
|
|
393
393
|
continue;
|
|
394
394
|
const snakeKey = (0, schema_js_1.camelToSnake)(key);
|
|
@@ -400,7 +400,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
400
400
|
}
|
|
401
401
|
const meta = qi.schema.tables[qi.table];
|
|
402
402
|
let relOrdCounter = 0;
|
|
403
|
-
return
|
|
403
|
+
return (0, filters_js_1.orderByEntries)(orderBy)
|
|
404
404
|
.map(([key, value]) => {
|
|
405
405
|
// Vector KNN ordering: { distance: { to, metric, direction? } }
|
|
406
406
|
if ((0, filters_js_1.isVectorOrderBy)(value)) {
|
|
@@ -1452,7 +1452,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
1452
1452
|
// An orderBy with no defined entries (`orderBy: {}`) is treated as absent —
|
|
1453
1453
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
1454
1454
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
1455
|
-
const relOrderEntries = spec !== true && spec.orderBy ?
|
|
1455
|
+
const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
1456
1456
|
const willWrap = relDef.type === 'hasMany' && spec !== true && (spec.limit !== undefined || relOrderEntries.length > 0);
|
|
1457
1457
|
// manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
|
|
1458
1458
|
// where, orderBy, and select/omit are handled there (the target alias is the
|
|
@@ -1623,7 +1623,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
1623
1623
|
// `orderBy: {}` (no defined entries) is treated as absent: it must not
|
|
1624
1624
|
// render a dangling `ORDER BY `. Param pushes here land BEFORE the
|
|
1625
1625
|
// spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
|
|
1626
|
-
const relOrderEntries = spec !== true && spec.orderBy ?
|
|
1626
|
+
const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
1627
1627
|
let orderClause = '';
|
|
1628
1628
|
if (relOrderEntries.length > 0) {
|
|
1629
1629
|
orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* turbine init — Initialize a Turbine project
|
|
7
7
|
* turbine generate | pull — Introspect database and generate TypeScript types
|
|
8
8
|
* turbine push - Apply schema-builder definitions to database (destructive ops gated)
|
|
9
|
-
* turbine migrate create <name> - Create a new SQL migration file (--auto | --recipe <name>)
|
|
9
|
+
* turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
10
10
|
* turbine migrate up — Apply pending migrations
|
|
11
11
|
* turbine migrate deploy — Apply pending migrations without prompts
|
|
12
12
|
* turbine migrate down — Rollback last migration
|
|
@@ -38,12 +38,24 @@ export interface CliArgs {
|
|
|
38
38
|
verbose?: boolean;
|
|
39
39
|
help?: boolean;
|
|
40
40
|
auto?: boolean;
|
|
41
|
+
/** `migrate create --from-diff`: scaffold UP/DOWN from the schema diff, destructive statements flagged. */
|
|
42
|
+
fromDiff?: boolean;
|
|
41
43
|
allowDrift?: boolean;
|
|
42
44
|
allowEmpty?: boolean;
|
|
43
45
|
allowDestructive?: boolean;
|
|
44
46
|
/** `migrate create --recipe <name>` scaffold selector. */
|
|
45
47
|
recipe?: string;
|
|
46
48
|
fix?: boolean;
|
|
49
|
+
/** `init --yes`/`-y`: accept every step's default non-interactively. */
|
|
50
|
+
yes?: boolean;
|
|
51
|
+
/** `init --skip-schema`: don't scaffold the schema file. */
|
|
52
|
+
skipSchema?: boolean;
|
|
53
|
+
/** `init --skip-seed`: don't scaffold the seed file or offer to run it. */
|
|
54
|
+
skipSeed?: boolean;
|
|
55
|
+
/** `init --skip-push`: don't offer to push the schema to the database. */
|
|
56
|
+
skipPush?: boolean;
|
|
57
|
+
/** `init --skip-generate`: don't offer to generate the typed client. */
|
|
58
|
+
skipGenerate?: boolean;
|
|
47
59
|
zod?: boolean;
|
|
48
60
|
includeViews?: boolean;
|
|
49
61
|
/** Omit the `Generated at:` header line for reproducible (diff-stable) output. */
|
|
@@ -129,6 +141,54 @@ export declare function dotEnvUrlConflictWarning(input: {
|
|
|
129
141
|
* project, and `'none'` when there is no readable/parseable package.json.
|
|
130
142
|
*/
|
|
131
143
|
export declare function detectConsumerModuleType(cwd?: string): 'module' | 'commonjs' | 'none';
|
|
144
|
+
/** A single step in the `turbine init` flow. */
|
|
145
|
+
export type InitStepId = 'config' | 'schema' | 'seed-file' | 'push' | 'generate' | 'seed-run';
|
|
146
|
+
/** What the planner decided to do with a step. */
|
|
147
|
+
export type InitStepAction = 'run' | 'prompt' | 'skip';
|
|
148
|
+
/** Why a step was skipped (only set when `action` is `skip`). */
|
|
149
|
+
export type InitStepSkipReason = 'exists' | 'flag' | 'no-url' | 'unreachable' | 'no-seed-file' | 'non-interactive' | 'default-no';
|
|
150
|
+
export interface InitPlanStep {
|
|
151
|
+
id: InitStepId;
|
|
152
|
+
action: InitStepAction;
|
|
153
|
+
/** Prompt default; also the value used to decide auto-run under `--yes`. */
|
|
154
|
+
defaultYes: boolean;
|
|
155
|
+
skipReason?: InitStepSkipReason;
|
|
156
|
+
}
|
|
157
|
+
/** Detected project state (all IO done by the caller). */
|
|
158
|
+
export interface InitPlanState {
|
|
159
|
+
configExists: boolean;
|
|
160
|
+
schemaExists: boolean;
|
|
161
|
+
seedFileExists: boolean;
|
|
162
|
+
hasUrl: boolean;
|
|
163
|
+
dbReachable: boolean;
|
|
164
|
+
}
|
|
165
|
+
/** Effective flags for the planner. */
|
|
166
|
+
export interface InitPlanFlags {
|
|
167
|
+
yes: boolean;
|
|
168
|
+
force: boolean;
|
|
169
|
+
interactive: boolean;
|
|
170
|
+
skipSchema: boolean;
|
|
171
|
+
skipSeed: boolean;
|
|
172
|
+
skipPush: boolean;
|
|
173
|
+
skipGenerate: boolean;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Pure step planner for `turbine init`. Given the detected project state and the
|
|
177
|
+
* effective flags, decide for each step whether to run it, prompt for it, or
|
|
178
|
+
* skip it (and why). No IO: every input is precomputed by the caller: so the
|
|
179
|
+
* whole decision matrix is unit-testable without a TTY or a database.
|
|
180
|
+
*
|
|
181
|
+
* Three modes:
|
|
182
|
+
* - `prompt` (interactive TTY, no `--yes`): scaffold + DB steps are prompted.
|
|
183
|
+
* - `auto-yes` (`--yes`): accept each step's default; the yes-defaults run.
|
|
184
|
+
* - `auto-legacy` (non-TTY, no `--yes`): reproduce the pre-existing init
|
|
185
|
+
* behavior. Scaffold files + generate run; push + seed-run do not.
|
|
186
|
+
*
|
|
187
|
+
* Steps that create files (config, schema, seed) are skipped when the file
|
|
188
|
+
* already exists, so re-runs are safe. DB steps (push, generate, seed-run) are
|
|
189
|
+
* skipped when there is no URL or the database is unreachable.
|
|
190
|
+
*/
|
|
191
|
+
export declare function planInitSteps(state: InitPlanState, flags: InitPlanFlags): InitPlanStep[];
|
|
132
192
|
export declare function buildMigrateDeployOptions(_args: CliArgs): {
|
|
133
193
|
allowDrift: false;
|
|
134
194
|
allowDestructive: true;
|