turbine-orm 0.72.0 → 0.73.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 +6 -3
- package/dist/cjs/cli/index.d.ts +7 -0
- package/dist/cjs/cli/index.js +113 -0
- package/dist/cjs/powql.d.ts +17 -4
- package/dist/cjs/powql.js +43 -9
- package/dist/cjs/query/builder.d.ts +18 -0
- package/dist/cjs/query/builder.js +64 -12
- package/dist/cjs/query/compound-unique.d.ts +44 -0
- package/dist/cjs/query/compound-unique.js +119 -0
- package/dist/cjs/query/option-surface.d.ts +22 -0
- package/dist/cjs/query/option-surface.js +91 -0
- package/dist/cjs/query/types.d.ts +15 -1
- package/dist/cjs/query/utils.d.ts +23 -0
- package/dist/cjs/query/utils.js +57 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +113 -0
- package/dist/powql.d.ts +17 -4
- package/dist/powql.js +45 -11
- package/dist/query/builder.d.ts +18 -0
- package/dist/query/builder.js +66 -14
- package/dist/query/compound-unique.d.ts +44 -0
- package/dist/query/compound-unique.js +116 -0
- package/dist/query/option-surface.d.ts +22 -0
- package/dist/query/option-surface.js +90 -0
- package/dist/query/types.d.ts +15 -1
- package/dist/query/utils.d.ts +23 -0
- package/dist/query/utils.js +56 -0
- package/package.json +2 -1
- package/skills/turbine-orm/SKILL.md +247 -0
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
*/
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.expandCompoundUniqueWhere = expandCompoundUniqueWhere;
|
|
44
|
+
exports.assertWhereIdentifiesOneRow = assertWhereIdentifiesOneRow;
|
|
45
|
+
exports.uniqueKeyNames = uniqueKeyNames;
|
|
46
|
+
exports.whereIdentifiesOneRow = whereIdentifiesOneRow;
|
|
44
47
|
const errors_js_1 = require("../errors.js");
|
|
45
48
|
const filters_js_1 = require("./filters.js");
|
|
46
49
|
const utils_js_1 = require("./utils.js");
|
|
@@ -203,3 +206,119 @@ function expandCompoundUniqueWhere(meta, where) {
|
|
|
203
206
|
}
|
|
204
207
|
return result ?? where;
|
|
205
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Every column set that identifies AT MOST ONE ROW of this table.
|
|
211
|
+
*
|
|
212
|
+
* Same sources and the same partial-index rule as {@link syntheticKeyMap}, and
|
|
213
|
+
* in this module for that reason: "what identifies one row" is one question,
|
|
214
|
+
* and answering it in two places is how a compound selector comes to be
|
|
215
|
+
* accepted by the name and refused by its members (0.72.0 fixed exactly that).
|
|
216
|
+
* The difference is only the arity: a synthetic SELECTOR needs two or more
|
|
217
|
+
* columns to have a joined name, while a single-column unique identifies a row
|
|
218
|
+
* perfectly well.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Throw unless `where` identifies a single row. The refusal for `findUnique` on
|
|
222
|
+
* EVERY engine, message included.
|
|
223
|
+
*
|
|
224
|
+
* Shared rather than written twice because `PowqlInterface` is a parallel
|
|
225
|
+
* implementation: two copies of a rule this specific (which sources count as
|
|
226
|
+
* unique, whether a null identifies, which keys the message lists) is how two
|
|
227
|
+
* engines come to disagree about whether a query is valid, which is the exact
|
|
228
|
+
* divergence 0.64.0 and 0.72.0 were both spent on.
|
|
229
|
+
*
|
|
230
|
+
* The message lists the keys that WOULD work, because the fix is almost always
|
|
231
|
+
* one of them and a caller cannot be expected to know which columns the
|
|
232
|
+
* database considers unique. A table with no unique key at all gets its own
|
|
233
|
+
* sentence: no `where` satisfies this, and "name a unique key" is advice that
|
|
234
|
+
* person cannot take.
|
|
235
|
+
*/
|
|
236
|
+
function assertWhereIdentifiesOneRow(meta, table, where) {
|
|
237
|
+
if (whereIdentifiesOneRow(meta, where ?? {}))
|
|
238
|
+
return;
|
|
239
|
+
const field = (c) => meta.reverseColumnMap[c] ?? c;
|
|
240
|
+
const keys = uniqueKeyNames(meta).map((cols) => cols.length === 1 ? `\`${field(cols[0])}\`` : `\`{ ${cols.map(field).join(', ')} }\``);
|
|
241
|
+
const advice = keys.length > 0
|
|
242
|
+
? `Name a unique key (${keys.join(', ')}), or use \`findFirst\` if you meant "any row matching a filter".`
|
|
243
|
+
: `Table "${table}" declares no primary key and no unique constraint, so no \`where\` can identify one row ` +
|
|
244
|
+
'here. Use `findFirst` (add an `orderBy` to make which row it is deterministic).';
|
|
245
|
+
throw new errors_js_1.ValidationError(`[turbine] findUnique on "${table}" refused: the \`where\` clause does not identify a single row, ` +
|
|
246
|
+
`so this would return an arbitrary one of the rows that match. ${advice}`);
|
|
247
|
+
}
|
|
248
|
+
function uniqueKeyNames(meta) {
|
|
249
|
+
return dedupeColumnSets(uniqueColumnSets(meta));
|
|
250
|
+
}
|
|
251
|
+
/** Distinct column sets, preserving first-seen order (a PK is often also a declared unique). */
|
|
252
|
+
function dedupeColumnSets(sets) {
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const cols of sets) {
|
|
256
|
+
const sig = cols.join('\u0000');
|
|
257
|
+
if (seen.has(sig))
|
|
258
|
+
continue;
|
|
259
|
+
seen.add(sig);
|
|
260
|
+
out.push(cols);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function uniqueColumnSets(meta) {
|
|
265
|
+
const sets = [];
|
|
266
|
+
if (meta.primaryKey.length > 0)
|
|
267
|
+
sets.push(meta.primaryKey);
|
|
268
|
+
for (const uc of meta.uniqueColumns)
|
|
269
|
+
if (uc.length > 0)
|
|
270
|
+
sets.push(uc);
|
|
271
|
+
for (const idx of meta.indexes) {
|
|
272
|
+
if (idx.unique && !idx.docPath && !idx.partial && idx.columns.length > 0)
|
|
273
|
+
sets.push(idx.columns);
|
|
274
|
+
}
|
|
275
|
+
return sets;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True when `where` pins every column of at least one unique key to a single
|
|
279
|
+
* value, so the row it names is the row it gets.
|
|
280
|
+
*
|
|
281
|
+
* Deliberately reads only the TOP LEVEL of the user's where. A unique key
|
|
282
|
+
* buried inside an `OR` does not identify a row (the other branch matches
|
|
283
|
+
* whatever it matches), and one inside an `AND` array is a shape nobody writes
|
|
284
|
+
* for a lookup by identity. Extra predicates alongside the key are fine: they
|
|
285
|
+
* can only narrow a set that already holds at most one row.
|
|
286
|
+
*
|
|
287
|
+
* A NULL is not an identity. `WHERE email IS NULL` matches every row whose
|
|
288
|
+
* email is null, which a UNIQUE constraint permits any number of, so a null
|
|
289
|
+
* value satisfies no key here even on a unique column.
|
|
290
|
+
*/
|
|
291
|
+
function whereIdentifiesOneRow(meta, where) {
|
|
292
|
+
const pinned = new Set();
|
|
293
|
+
for (const [key, value] of Object.entries(where)) {
|
|
294
|
+
if (!isPinnedToOneValue(value))
|
|
295
|
+
continue;
|
|
296
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
297
|
+
if (column !== undefined)
|
|
298
|
+
pinned.add(column);
|
|
299
|
+
}
|
|
300
|
+
if (pinned.size === 0)
|
|
301
|
+
return false;
|
|
302
|
+
return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
|
|
303
|
+
}
|
|
304
|
+
/** A bare value, or an operator object whose `equals` is a value. */
|
|
305
|
+
function isPinnedToOneValue(value) {
|
|
306
|
+
if (value === undefined || value === null)
|
|
307
|
+
return false;
|
|
308
|
+
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
309
|
+
const eq = value.equals;
|
|
310
|
+
return eq !== undefined && eq !== null;
|
|
311
|
+
}
|
|
312
|
+
// A JSON / array / vector filter narrows, it does not identify.
|
|
313
|
+
if ((0, filters_js_1.isJsonFilter)(value) || (0, filters_js_1.isArrayFilter)(value) || (0, filters_js_1.isVectorFilter)(value))
|
|
314
|
+
return false;
|
|
315
|
+
// Anything else that is a plain object is a relation filter or a sub-where,
|
|
316
|
+
// neither of which pins a column. Dates, Buffers and primitives are values.
|
|
317
|
+
return !isPlainObject(value);
|
|
318
|
+
}
|
|
319
|
+
function isPlainObject(value) {
|
|
320
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
321
|
+
return false;
|
|
322
|
+
const proto = Object.getPrototypeOf(value);
|
|
323
|
+
return proto === Object.prototype || proto === null;
|
|
324
|
+
}
|
|
@@ -108,4 +108,26 @@ export declare const ALL_OPTION_TABLES: Readonly<Record<string, Readonly<Record<
|
|
|
108
108
|
export declare function applyNativeOptions(table: Readonly<Record<string, OptionKind>>, src: Record<string, unknown>, dst: Record<string, unknown>): void;
|
|
109
109
|
/** The keys of `table` with the given kind, as a set. */
|
|
110
110
|
export declare function optionKeysOfKind(table: Readonly<Record<string, OptionKind>>, ...kinds: OptionKind[]): string[];
|
|
111
|
+
/**
|
|
112
|
+
* Dev-mode warning for a key that is not part of the operation's option
|
|
113
|
+
* surface, and is therefore doing nothing.
|
|
114
|
+
*
|
|
115
|
+
* The motivating case is `include`. It is Prisma's word for `with`, it is what
|
|
116
|
+
* a model or a developer coming from Prisma reaches for first, and an
|
|
117
|
+
* unrecognized key is simply ignored: the query runs, returns rows, and the
|
|
118
|
+
* relation the caller asked for is absent. No error, no empty array, just a
|
|
119
|
+
* missing key on every row. A cross-model eval measured this as the single
|
|
120
|
+
* largest source of confidently-wrong queries against Turbine, and every one of
|
|
121
|
+
* them looked like a success from inside the process.
|
|
122
|
+
*
|
|
123
|
+
* A WARNING and never an error, deliberately. Refusing an unknown key would
|
|
124
|
+
* break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
|
|
125
|
+
* option surface grows: a caller pinned to an older minor would have their
|
|
126
|
+
* working query start throwing. A warning costs a correct program nothing and
|
|
127
|
+
* tells an incorrect one exactly what happened.
|
|
128
|
+
*
|
|
129
|
+
* Dev-only, once per `table.operation.key` per process, and total: the whole
|
|
130
|
+
* body is wrapped, because a diagnostic must never be the reason a query fails.
|
|
131
|
+
*/
|
|
132
|
+
export declare function warnUnknownQueryOptions(table: string, operation: string, args: unknown): void;
|
|
111
133
|
export {};
|
|
@@ -56,6 +56,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
56
56
|
exports.ALL_OPTION_TABLES = exports.GROUP_BY_OPTIONS = exports.AGGREGATE_OPTIONS = exports.COUNT_OPTIONS = exports.UPSERT_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.UPDATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = void 0;
|
|
57
57
|
exports.applyNativeOptions = applyNativeOptions;
|
|
58
58
|
exports.optionKeysOfKind = optionKeysOfKind;
|
|
59
|
+
exports.warnUnknownQueryOptions = warnUnknownQueryOptions;
|
|
60
|
+
// Runtime imports, and the only ones in this file: the unknown-key warning at
|
|
61
|
+
// the bottom needs the once-per-process registry and the name suggester. Both
|
|
62
|
+
// are leaves that do not import this module, so the type-only shape of
|
|
63
|
+
// everything above is unaffected.
|
|
64
|
+
const utils_js_1 = require("./utils.js");
|
|
65
|
+
const warn_registry_js_1 = require("./warn-registry.js");
|
|
59
66
|
exports.FIND_UNIQUE_OPTIONS = {
|
|
60
67
|
where: 'prisma',
|
|
61
68
|
select: 'prisma',
|
|
@@ -84,7 +91,13 @@ exports.FIND_MANY_OPTIONS = {
|
|
|
84
91
|
omit: 'prisma',
|
|
85
92
|
orderBy: 'prisma',
|
|
86
93
|
cursor: 'prisma',
|
|
94
|
+
// `take` and `skip` are Prisma's own spellings and core accepts them as
|
|
95
|
+
// aliases, but they stay hand-translated because Prisma gives them meanings
|
|
96
|
+
// core does not have: a NEGATIVE `take` pages from the end, and `skip: 1`
|
|
97
|
+
// beside a `cursor` is the exclusive-pagination idiom. Forwarding either
|
|
98
|
+
// verbatim would hand core a number that means something else.
|
|
87
99
|
take: 'prisma',
|
|
100
|
+
skip: 'prisma',
|
|
88
101
|
distinct: 'prisma',
|
|
89
102
|
relationLoadStrategy: 'prisma',
|
|
90
103
|
with: 'nativeAlias',
|
|
@@ -231,3 +244,81 @@ function applyNativeOptions(table, src, dst) {
|
|
|
231
244
|
function optionKeysOfKind(table, ...kinds) {
|
|
232
245
|
return Object.keys(table).filter((k) => kinds.includes(table[k]));
|
|
233
246
|
}
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Unknown-key diagnostic
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
/**
|
|
251
|
+
* The operations a caller can invoke, mapped to the table that describes their
|
|
252
|
+
* legal keys. The `*OrThrow` and `findFirst` variants take the same args as the
|
|
253
|
+
* method they are built on, so they share its table rather than getting a copy
|
|
254
|
+
* that could drift from it.
|
|
255
|
+
*/
|
|
256
|
+
const OPERATION_TABLE = {
|
|
257
|
+
...exports.ALL_OPTION_TABLES,
|
|
258
|
+
findFirst: exports.FIND_MANY_OPTIONS,
|
|
259
|
+
findFirstOrThrow: exports.FIND_MANY_OPTIONS,
|
|
260
|
+
findUniqueOrThrow: exports.FIND_UNIQUE_OPTIONS,
|
|
261
|
+
findManyStreamBatches: exports.FIND_MANY_STREAM_OPTIONS,
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Prisma spellings that name a real Turbine option under a different word.
|
|
265
|
+
*
|
|
266
|
+
* Only spellings whose Turbine equivalent EXISTS belong here: the message tells
|
|
267
|
+
* the caller what to write instead, so a key with no equivalent would produce
|
|
268
|
+
* advice that does not work. `take` / `skip` were once on this list and are now
|
|
269
|
+
* accepted outright.
|
|
270
|
+
*/
|
|
271
|
+
const PRISMA_SPELLING = {
|
|
272
|
+
include: 'with',
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Dev-mode warning for a key that is not part of the operation's option
|
|
276
|
+
* surface, and is therefore doing nothing.
|
|
277
|
+
*
|
|
278
|
+
* The motivating case is `include`. It is Prisma's word for `with`, it is what
|
|
279
|
+
* a model or a developer coming from Prisma reaches for first, and an
|
|
280
|
+
* unrecognized key is simply ignored: the query runs, returns rows, and the
|
|
281
|
+
* relation the caller asked for is absent. No error, no empty array, just a
|
|
282
|
+
* missing key on every row. A cross-model eval measured this as the single
|
|
283
|
+
* largest source of confidently-wrong queries against Turbine, and every one of
|
|
284
|
+
* them looked like a success from inside the process.
|
|
285
|
+
*
|
|
286
|
+
* A WARNING and never an error, deliberately. Refusing an unknown key would
|
|
287
|
+
* break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
|
|
288
|
+
* option surface grows: a caller pinned to an older minor would have their
|
|
289
|
+
* working query start throwing. A warning costs a correct program nothing and
|
|
290
|
+
* tells an incorrect one exactly what happened.
|
|
291
|
+
*
|
|
292
|
+
* Dev-only, once per `table.operation.key` per process, and total: the whole
|
|
293
|
+
* body is wrapped, because a diagnostic must never be the reason a query fails.
|
|
294
|
+
*/
|
|
295
|
+
function warnUnknownQueryOptions(table, operation, args) {
|
|
296
|
+
if (process.env.NODE_ENV === 'production')
|
|
297
|
+
return;
|
|
298
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args))
|
|
299
|
+
return;
|
|
300
|
+
const known = OPERATION_TABLE[operation];
|
|
301
|
+
if (!known)
|
|
302
|
+
return;
|
|
303
|
+
try {
|
|
304
|
+
for (const key of Object.keys(args)) {
|
|
305
|
+
// `{ ...maybeOptions }` routinely materializes keys with no value.
|
|
306
|
+
// Nothing is being ignored when the value is undefined.
|
|
307
|
+
if (args[key] === undefined)
|
|
308
|
+
continue;
|
|
309
|
+
if (Object.hasOwn(known, key))
|
|
310
|
+
continue;
|
|
311
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unknownQueryOption, `${table}.${operation}.${key}`))
|
|
312
|
+
continue;
|
|
313
|
+
const prismaSpelling = PRISMA_SPELLING[key];
|
|
314
|
+
const suggestion = prismaSpelling ?? (0, utils_js_1.suggestKey)(key, Object.keys(known));
|
|
315
|
+
const because = prismaSpelling ? ` Turbine spells this "${prismaSpelling}".` : '';
|
|
316
|
+
console.warn(`[turbine] unknown option "${key}" in ${table}.${operation}(), it is ignored.${because}` +
|
|
317
|
+
(!prismaSpelling && suggestion ? ` Did you mean "${suggestion}"?` : ''));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Key enumeration is the only thing that can fail here (a Proxy whose
|
|
322
|
+
// ownKeys throws), and it must not take the query with it.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -629,8 +629,22 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
|
|
|
629
629
|
with?: W;
|
|
630
630
|
/** Cursor-based pagination: start after this row */
|
|
631
631
|
cursor?: Partial<T>;
|
|
632
|
-
/**
|
|
632
|
+
/**
|
|
633
|
+
* Prisma's spelling of {@link FindManyArgs.limit}. Folded into `limit` before
|
|
634
|
+
* anything reads it; passing both with different values is a
|
|
635
|
+
* `ValidationError`.
|
|
636
|
+
*/
|
|
633
637
|
take?: number;
|
|
638
|
+
/**
|
|
639
|
+
* Prisma's spelling of {@link FindManyArgs.offset}. Folded into `offset`
|
|
640
|
+
* before anything reads it; passing both with different values is a
|
|
641
|
+
* `ValidationError`.
|
|
642
|
+
*
|
|
643
|
+
* Accepted since 0.73.0. Before that `take` was recognized and `skip` was
|
|
644
|
+
* not, so the Prisma pair `{ take, skip }` silently returned the first page
|
|
645
|
+
* however far the caller thought they had paged.
|
|
646
|
+
*/
|
|
647
|
+
skip?: number;
|
|
634
648
|
/** De-duplicate results by specified fields */
|
|
635
649
|
distinct?: (keyof T & string)[];
|
|
636
650
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
@@ -728,3 +728,26 @@ export declare function warnRedundantSortTerm(table: string, clause: string, dro
|
|
|
728
728
|
*/
|
|
729
729
|
export declare function selectNamesNothingMessage(table: string): string;
|
|
730
730
|
export declare function selectOmitExclusiveMessage(table: string): string;
|
|
731
|
+
/**
|
|
732
|
+
* The Prisma pagination aliases, folded into Turbine's own spelling ONCE,
|
|
733
|
+
* before anything reads them.
|
|
734
|
+
*
|
|
735
|
+
* Turbine's names are `limit` / `offset`; Prisma's are `take` / `skip`. `take`
|
|
736
|
+
* was accepted and `skip` was not, which is the worst of the three possible
|
|
737
|
+
* states: `{ take: 20, skip: 40 }` is what a Prisma habit writes, it looks
|
|
738
|
+
* accepted because half of it is, and the query silently returns page one
|
|
739
|
+
* forever. An unknown key is at least inert on its own; a HALF-recognized pair
|
|
740
|
+
* changes the answer.
|
|
741
|
+
*
|
|
742
|
+
* Folded here rather than read at each site deliberately. `take` used to be
|
|
743
|
+
* handled by six separate `args?.take ?? args?.limit` reads, one of which is
|
|
744
|
+
* the SQL-cache FINGERPRINT, so adding `skip` the same way would have meant
|
|
745
|
+
* teaching six places about it and a miss in the fingerprint is not a missing
|
|
746
|
+
* feature, it is two different pages sharing one cached statement. Normalizing
|
|
747
|
+
* up front leaves `limit` / `offset` as the single authority and the aliases
|
|
748
|
+
* cease to exist below this line.
|
|
749
|
+
*
|
|
750
|
+
* Returns the SAME object when neither alias is present, so the common path
|
|
751
|
+
* allocates nothing.
|
|
752
|
+
*/
|
|
753
|
+
export declare function normalizePagination<A extends object | undefined>(args: A): A;
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -49,7 +49,9 @@ exports.relationInProjectionMessage = relationInProjectionMessage;
|
|
|
49
49
|
exports.warnRedundantSortTerm = warnRedundantSortTerm;
|
|
50
50
|
exports.selectNamesNothingMessage = selectNamesNothingMessage;
|
|
51
51
|
exports.selectOmitExclusiveMessage = selectOmitExclusiveMessage;
|
|
52
|
+
exports.normalizePagination = normalizePagination;
|
|
52
53
|
const pg_1 = __importDefault(require("pg"));
|
|
54
|
+
const errors_js_1 = require("../errors.js");
|
|
53
55
|
const schema_js_1 = require("../schema.js");
|
|
54
56
|
const warn_registry_js_1 = require("./warn-registry.js");
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
@@ -1539,3 +1541,58 @@ function selectOmitExclusiveMessage(table) {
|
|
|
1539
1541
|
return (`[turbine] "select" and "omit" are mutually exclusive (on table "${table}"). ` +
|
|
1540
1542
|
`A select already lists exactly the fields you want.`);
|
|
1541
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* The Prisma pagination aliases, folded into Turbine's own spelling ONCE,
|
|
1546
|
+
* before anything reads them.
|
|
1547
|
+
*
|
|
1548
|
+
* Turbine's names are `limit` / `offset`; Prisma's are `take` / `skip`. `take`
|
|
1549
|
+
* was accepted and `skip` was not, which is the worst of the three possible
|
|
1550
|
+
* states: `{ take: 20, skip: 40 }` is what a Prisma habit writes, it looks
|
|
1551
|
+
* accepted because half of it is, and the query silently returns page one
|
|
1552
|
+
* forever. An unknown key is at least inert on its own; a HALF-recognized pair
|
|
1553
|
+
* changes the answer.
|
|
1554
|
+
*
|
|
1555
|
+
* Folded here rather than read at each site deliberately. `take` used to be
|
|
1556
|
+
* handled by six separate `args?.take ?? args?.limit` reads, one of which is
|
|
1557
|
+
* the SQL-cache FINGERPRINT, so adding `skip` the same way would have meant
|
|
1558
|
+
* teaching six places about it and a miss in the fingerprint is not a missing
|
|
1559
|
+
* feature, it is two different pages sharing one cached statement. Normalizing
|
|
1560
|
+
* up front leaves `limit` / `offset` as the single authority and the aliases
|
|
1561
|
+
* cease to exist below this line.
|
|
1562
|
+
*
|
|
1563
|
+
* Returns the SAME object when neither alias is present, so the common path
|
|
1564
|
+
* allocates nothing.
|
|
1565
|
+
*/
|
|
1566
|
+
function normalizePagination(args) {
|
|
1567
|
+
if (!args)
|
|
1568
|
+
return args;
|
|
1569
|
+
const a = args;
|
|
1570
|
+
if (a.take === undefined && a.skip === undefined)
|
|
1571
|
+
return args;
|
|
1572
|
+
const out = { ...args };
|
|
1573
|
+
if (a.take !== undefined) {
|
|
1574
|
+
assertAliasAgrees('take', a.take, 'limit', a.limit);
|
|
1575
|
+
out.limit = a.take;
|
|
1576
|
+
delete out.take;
|
|
1577
|
+
}
|
|
1578
|
+
if (a.skip !== undefined) {
|
|
1579
|
+
assertAliasAgrees('skip', a.skip, 'offset', a.offset);
|
|
1580
|
+
out.offset = a.skip;
|
|
1581
|
+
delete out.skip;
|
|
1582
|
+
}
|
|
1583
|
+
return out;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Both spellings of one bound, disagreeing. Refused rather than resolved: the
|
|
1587
|
+
* old `take ?? limit` silently preferred one of the two numbers the caller
|
|
1588
|
+
* wrote, and there is no reading of `{ limit: 10, take: 5 }` that makes one of
|
|
1589
|
+
* them the intended answer. Equal values are accepted, since there is nothing
|
|
1590
|
+
* to choose between.
|
|
1591
|
+
*/
|
|
1592
|
+
function assertAliasAgrees(alias, aliasValue, native, nativeValue) {
|
|
1593
|
+
if (nativeValue === undefined || nativeValue === aliasValue)
|
|
1594
|
+
return;
|
|
1595
|
+
throw new errors_js_1.ValidationError(`[turbine] "${alias}" and "${native}" are the same option and were given different values ` +
|
|
1596
|
+
`(${alias}: ${String(aliasValue)}, ${native}: ${String(nativeValue)}). ` +
|
|
1597
|
+
`"${alias}" is Prisma's spelling of "${native}"; pass one of them.`);
|
|
1598
|
+
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
19
19
|
* turbine mcp , Start read-only MCP server over JSON-RPC stdio
|
|
20
20
|
* turbine observe , Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
21
|
+
* turbine skill - Install the agent query skill (--print, --agents, --dir <path>)
|
|
21
22
|
*
|
|
22
23
|
* Usage:
|
|
23
24
|
* DATABASE_URL=postgres://... npx turbine generate
|
|
@@ -113,6 +114,12 @@ export interface CliArgs {
|
|
|
113
114
|
* not be turned into a failed install.
|
|
114
115
|
*/
|
|
115
116
|
ifDb?: boolean;
|
|
117
|
+
/** `skill --print`: write the skill to stdout instead of installing it. */
|
|
118
|
+
print?: boolean;
|
|
119
|
+
/** `skill --agents`: print the AGENTS.md instructions block instead. */
|
|
120
|
+
agents?: boolean;
|
|
121
|
+
/** `skill --dir <path>`: the skills root to install into (default `.claude/skills`). */
|
|
122
|
+
dir?: string;
|
|
116
123
|
}
|
|
117
124
|
export declare function parseArgs(argv?: string[]): CliArgs;
|
|
118
125
|
/**
|
package/dist/cli/index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
19
19
|
* turbine mcp , Start read-only MCP server over JSON-RPC stdio
|
|
20
20
|
* turbine observe , Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
21
|
+
* turbine skill - Install the agent query skill (--print, --agents, --dir <path>)
|
|
21
22
|
*
|
|
22
23
|
* Usage:
|
|
23
24
|
* DATABASE_URL=postgres://... npx turbine generate
|
|
@@ -171,6 +172,17 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
171
172
|
case '--allow-pooler':
|
|
172
173
|
result.allowPooler = true;
|
|
173
174
|
break;
|
|
175
|
+
// `turbine skill`
|
|
176
|
+
case '--print':
|
|
177
|
+
result.print = true;
|
|
178
|
+
break;
|
|
179
|
+
case '--agents':
|
|
180
|
+
result.agents = true;
|
|
181
|
+
break;
|
|
182
|
+
case '--dir':
|
|
183
|
+
result.dir = next;
|
|
184
|
+
i++;
|
|
185
|
+
break;
|
|
174
186
|
case '--zod':
|
|
175
187
|
result.zod = true;
|
|
176
188
|
break;
|
|
@@ -3945,6 +3957,7 @@ function showHelp() {
|
|
|
3945
3957
|
console.log(` ${cyan('studio')} Launch local read-only web UI ${dim('(--write for writes, --demo for a sample DB)')}`);
|
|
3946
3958
|
console.log(` ${cyan('mcp')} Start read-only MCP server over stdio`);
|
|
3947
3959
|
console.log(` ${cyan('observe')} Launch metrics dashboard ${dim('(requires TURBINE_OBSERVE_URL)')}`);
|
|
3960
|
+
console.log(` ${cyan('skill')} Install the agent query skill ${dim('(--print, --agents, --dir <path>)')}`);
|
|
3948
3961
|
newline();
|
|
3949
3962
|
console.log(` ${bold('Options:')}`);
|
|
3950
3963
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
@@ -3995,6 +4008,103 @@ function showHelp() {
|
|
|
3995
4008
|
newline();
|
|
3996
4009
|
}
|
|
3997
4010
|
// ---------------------------------------------------------------------------
|
|
4011
|
+
// Agent skill
|
|
4012
|
+
// ---------------------------------------------------------------------------
|
|
4013
|
+
/**
|
|
4014
|
+
* turbine-orm's own package root, found by walking up from the running script.
|
|
4015
|
+
*
|
|
4016
|
+
* `process.argv[1]` rather than `import.meta.url` so the same source compiles
|
|
4017
|
+
* for both the ESM and CJS builds, and realpath first because `npx turbine`
|
|
4018
|
+
* runs through a `node_modules/.bin` symlink whose dirname is the CONSUMER's
|
|
4019
|
+
* tree, where this package.json does not exist.
|
|
4020
|
+
*/
|
|
4021
|
+
function ownPackageRoot() {
|
|
4022
|
+
try {
|
|
4023
|
+
let entry = process.argv[1] ?? '';
|
|
4024
|
+
try {
|
|
4025
|
+
entry = realpathSync(entry);
|
|
4026
|
+
}
|
|
4027
|
+
catch {
|
|
4028
|
+
// keep the raw path if realpath fails (e.g. deleted cwd)
|
|
4029
|
+
}
|
|
4030
|
+
let dir = dirname(entry);
|
|
4031
|
+
for (let i = 0; i < 6; i++) {
|
|
4032
|
+
const candidate = resolve(dir, 'package.json');
|
|
4033
|
+
if (existsSync(candidate)) {
|
|
4034
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
4035
|
+
if (pkg.name === 'turbine-orm')
|
|
4036
|
+
return dir;
|
|
4037
|
+
}
|
|
4038
|
+
const parent = dirname(dir);
|
|
4039
|
+
if (parent === dir)
|
|
4040
|
+
break;
|
|
4041
|
+
dir = parent;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
catch {
|
|
4045
|
+
// fall through
|
|
4046
|
+
}
|
|
4047
|
+
return undefined;
|
|
4048
|
+
}
|
|
4049
|
+
/** The instructions block for a project's AGENTS.md / CLAUDE.md. */
|
|
4050
|
+
const AGENTS_SNIPPET = `## Database access (Turbine ORM)
|
|
4051
|
+
|
|
4052
|
+
- Queries go through the generated client. Read \`generated/turbine/types.ts\` for
|
|
4053
|
+
the entity and input types before writing one; the names there are the truth.
|
|
4054
|
+
- Relations are \`with\`, never Prisma's \`include\`. An unrecognized option is
|
|
4055
|
+
ignored, so an \`include\` returns rows with the relation missing.
|
|
4056
|
+
- \`select\` and \`omit\` name columns only. A relation named in \`select\` throws.
|
|
4057
|
+
- \`findUnique\` needs a unique key. Use \`findFirst\` for "any row matching a
|
|
4058
|
+
filter", with an \`orderBy\` if which row matters.
|
|
4059
|
+
- Re-run \`npx turbine generate\` after any schema change, then \`tsc --noEmit\`:
|
|
4060
|
+
an invalid query is a type error, so the type checker is the fastest reviewer.
|
|
4061
|
+
- Never write \`includePii: true\`, \`skipGlobalFilters: true\` or
|
|
4062
|
+
\`allowFullTableScan: true\`. Those options take an imported \`UNSAFE\` symbol and
|
|
4063
|
+
nothing else; any other value throws.
|
|
4064
|
+
- Full query reference: https://turbineorm.dev/llms.txt
|
|
4065
|
+
`;
|
|
4066
|
+
/**
|
|
4067
|
+
* Install the packaged query-writing skill into the project.
|
|
4068
|
+
*
|
|
4069
|
+
* The skill is a file in the published tarball rather than something generated
|
|
4070
|
+
* here, so what an agent reads is exactly what the repository tests: every
|
|
4071
|
+
* factual claim in it is executed against a live database by
|
|
4072
|
+
* `evals/src/verify-skill.ts` on each release.
|
|
4073
|
+
*/
|
|
4074
|
+
function cmdSkill(args) {
|
|
4075
|
+
if (args.agents === true) {
|
|
4076
|
+
console.log(AGENTS_SNIPPET);
|
|
4077
|
+
return;
|
|
4078
|
+
}
|
|
4079
|
+
const root = ownPackageRoot();
|
|
4080
|
+
const source = root ? resolve(root, 'skills', 'turbine-orm', 'SKILL.md') : undefined;
|
|
4081
|
+
if (!source || !existsSync(source)) {
|
|
4082
|
+
error('Could not find the packaged skill inside turbine-orm.');
|
|
4083
|
+
newline();
|
|
4084
|
+
console.log(` ${dim('Read it online instead:')} ${cyan('https://turbineorm.dev/ai-agents')}`);
|
|
4085
|
+
newline();
|
|
4086
|
+
process.exit(1);
|
|
4087
|
+
}
|
|
4088
|
+
const body = readFileSync(source, 'utf8');
|
|
4089
|
+
if (args.print === true) {
|
|
4090
|
+
process.stdout.write(body);
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
const skillsDir = args.dir ?? join('.claude', 'skills');
|
|
4094
|
+
const target = resolve(process.cwd(), skillsDir, 'turbine-orm', 'SKILL.md');
|
|
4095
|
+
const existed = existsSync(target);
|
|
4096
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
4097
|
+
writeFileSync(target, body);
|
|
4098
|
+
newline();
|
|
4099
|
+
success(`${existed ? 'Updated' : 'Installed'} the Turbine query skill`);
|
|
4100
|
+
console.log(` ${dim(relative(process.cwd(), target))}`);
|
|
4101
|
+
newline();
|
|
4102
|
+
console.log(` ${dim('Also worth doing:')}`);
|
|
4103
|
+
console.log(` ${dim('-')} connect the read-only MCP server: ${cyan('npx turbine mcp')}`);
|
|
4104
|
+
console.log(` ${dim('-')} add the instructions block to AGENTS.md: ${cyan('npx turbine skill --agents')}`);
|
|
4105
|
+
newline();
|
|
4106
|
+
}
|
|
4107
|
+
// ---------------------------------------------------------------------------
|
|
3998
4108
|
// Version
|
|
3999
4109
|
// ---------------------------------------------------------------------------
|
|
4000
4110
|
function showVersion() {
|
|
@@ -4143,6 +4253,9 @@ async function main() {
|
|
|
4143
4253
|
case 'observe':
|
|
4144
4254
|
await cmdObserve(args);
|
|
4145
4255
|
break;
|
|
4256
|
+
case 'skill':
|
|
4257
|
+
cmdSkill(args);
|
|
4258
|
+
break;
|
|
4146
4259
|
default:
|
|
4147
4260
|
error(`Unknown command: ${bold(args.command)}`);
|
|
4148
4261
|
newline();
|
package/dist/powql.d.ts
CHANGED
|
@@ -148,10 +148,11 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
148
148
|
*/
|
|
149
149
|
private get capabilities();
|
|
150
150
|
/**
|
|
151
|
-
* The `limit` a query actually emits: the explicit `limit`,
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* The `limit` a query actually emits: the explicit `limit`, then the
|
|
152
|
+
* client-level `defaultLimit`. Prisma's `take` alias is already folded into
|
|
153
|
+
* `limit` by `normalizeArgs`, so there is one spelling by the time this runs.
|
|
154
|
+
* Shared by {@link buildFind} and the {@link findMany} zero short-circuit so
|
|
155
|
+
* the two can never disagree about which limit is in force.
|
|
155
156
|
*/
|
|
156
157
|
private effectiveLimit;
|
|
157
158
|
/**
|
|
@@ -186,7 +187,19 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
186
187
|
* which is the divergence class the projection resolver already cost.
|
|
187
188
|
*/
|
|
188
189
|
private withDeclaredRelationNames;
|
|
190
|
+
/**
|
|
191
|
+
* Caller args in canonical form: declared relation spellings, and `take` /
|
|
192
|
+
* `skip` folded into `limit` / `offset`.
|
|
193
|
+
*
|
|
194
|
+
* Same composition as `QueryInterface.normalizeArgs` and here for the same
|
|
195
|
+
* reason the method above is here: nothing about a parallel implementation
|
|
196
|
+
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
197
|
+
* not `skip` pages differently from one that reads both.
|
|
198
|
+
*/
|
|
199
|
+
private normalizeArgs;
|
|
189
200
|
private assertNoForceCustomPlan;
|
|
201
|
+
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
202
|
+
private assertIdentifiesOneRow;
|
|
190
203
|
private assertPagination;
|
|
191
204
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
|
192
205
|
private alwaysFalse;
|