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
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
*
|
|
52
52
|
* @module
|
|
53
53
|
*/
|
|
54
|
+
// Runtime imports, and the only ones in this file: the unknown-key warning at
|
|
55
|
+
// the bottom needs the once-per-process registry and the name suggester. Both
|
|
56
|
+
// are leaves that do not import this module, so the type-only shape of
|
|
57
|
+
// everything above is unaffected.
|
|
58
|
+
import { suggestKey } from './utils.js';
|
|
59
|
+
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
54
60
|
export const FIND_UNIQUE_OPTIONS = {
|
|
55
61
|
where: 'prisma',
|
|
56
62
|
select: 'prisma',
|
|
@@ -79,7 +85,13 @@ export const FIND_MANY_OPTIONS = {
|
|
|
79
85
|
omit: 'prisma',
|
|
80
86
|
orderBy: 'prisma',
|
|
81
87
|
cursor: 'prisma',
|
|
88
|
+
// `take` and `skip` are Prisma's own spellings and core accepts them as
|
|
89
|
+
// aliases, but they stay hand-translated because Prisma gives them meanings
|
|
90
|
+
// core does not have: a NEGATIVE `take` pages from the end, and `skip: 1`
|
|
91
|
+
// beside a `cursor` is the exclusive-pagination idiom. Forwarding either
|
|
92
|
+
// verbatim would hand core a number that means something else.
|
|
82
93
|
take: 'prisma',
|
|
94
|
+
skip: 'prisma',
|
|
83
95
|
distinct: 'prisma',
|
|
84
96
|
relationLoadStrategy: 'prisma',
|
|
85
97
|
with: 'nativeAlias',
|
|
@@ -226,3 +238,81 @@ export function applyNativeOptions(table, src, dst) {
|
|
|
226
238
|
export function optionKeysOfKind(table, ...kinds) {
|
|
227
239
|
return Object.keys(table).filter((k) => kinds.includes(table[k]));
|
|
228
240
|
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Unknown-key diagnostic
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
/**
|
|
245
|
+
* The operations a caller can invoke, mapped to the table that describes their
|
|
246
|
+
* legal keys. The `*OrThrow` and `findFirst` variants take the same args as the
|
|
247
|
+
* method they are built on, so they share its table rather than getting a copy
|
|
248
|
+
* that could drift from it.
|
|
249
|
+
*/
|
|
250
|
+
const OPERATION_TABLE = {
|
|
251
|
+
...ALL_OPTION_TABLES,
|
|
252
|
+
findFirst: FIND_MANY_OPTIONS,
|
|
253
|
+
findFirstOrThrow: FIND_MANY_OPTIONS,
|
|
254
|
+
findUniqueOrThrow: FIND_UNIQUE_OPTIONS,
|
|
255
|
+
findManyStreamBatches: FIND_MANY_STREAM_OPTIONS,
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Prisma spellings that name a real Turbine option under a different word.
|
|
259
|
+
*
|
|
260
|
+
* Only spellings whose Turbine equivalent EXISTS belong here: the message tells
|
|
261
|
+
* the caller what to write instead, so a key with no equivalent would produce
|
|
262
|
+
* advice that does not work. `take` / `skip` were once on this list and are now
|
|
263
|
+
* accepted outright.
|
|
264
|
+
*/
|
|
265
|
+
const PRISMA_SPELLING = {
|
|
266
|
+
include: 'with',
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Dev-mode warning for a key that is not part of the operation's option
|
|
270
|
+
* surface, and is therefore doing nothing.
|
|
271
|
+
*
|
|
272
|
+
* The motivating case is `include`. It is Prisma's word for `with`, it is what
|
|
273
|
+
* a model or a developer coming from Prisma reaches for first, and an
|
|
274
|
+
* unrecognized key is simply ignored: the query runs, returns rows, and the
|
|
275
|
+
* relation the caller asked for is absent. No error, no empty array, just a
|
|
276
|
+
* missing key on every row. A cross-model eval measured this as the single
|
|
277
|
+
* largest source of confidently-wrong queries against Turbine, and every one of
|
|
278
|
+
* them looked like a success from inside the process.
|
|
279
|
+
*
|
|
280
|
+
* A WARNING and never an error, deliberately. Refusing an unknown key would
|
|
281
|
+
* break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
|
|
282
|
+
* option surface grows: a caller pinned to an older minor would have their
|
|
283
|
+
* working query start throwing. A warning costs a correct program nothing and
|
|
284
|
+
* tells an incorrect one exactly what happened.
|
|
285
|
+
*
|
|
286
|
+
* Dev-only, once per `table.operation.key` per process, and total: the whole
|
|
287
|
+
* body is wrapped, because a diagnostic must never be the reason a query fails.
|
|
288
|
+
*/
|
|
289
|
+
export function warnUnknownQueryOptions(table, operation, args) {
|
|
290
|
+
if (process.env.NODE_ENV === 'production')
|
|
291
|
+
return;
|
|
292
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args))
|
|
293
|
+
return;
|
|
294
|
+
const known = OPERATION_TABLE[operation];
|
|
295
|
+
if (!known)
|
|
296
|
+
return;
|
|
297
|
+
try {
|
|
298
|
+
for (const key of Object.keys(args)) {
|
|
299
|
+
// `{ ...maybeOptions }` routinely materializes keys with no value.
|
|
300
|
+
// Nothing is being ignored when the value is undefined.
|
|
301
|
+
if (args[key] === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
if (Object.hasOwn(known, key))
|
|
304
|
+
continue;
|
|
305
|
+
if (!shouldWarnOnce(WARN_NS.unknownQueryOption, `${table}.${operation}.${key}`))
|
|
306
|
+
continue;
|
|
307
|
+
const prismaSpelling = PRISMA_SPELLING[key];
|
|
308
|
+
const suggestion = prismaSpelling ?? suggestKey(key, Object.keys(known));
|
|
309
|
+
const because = prismaSpelling ? ` Turbine spells this "${prismaSpelling}".` : '';
|
|
310
|
+
console.warn(`[turbine] unknown option "${key}" in ${table}.${operation}(), it is ignored.${because}` +
|
|
311
|
+
(!prismaSpelling && suggestion ? ` Did you mean "${suggestion}"?` : ''));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Key enumeration is the only thing that can fail here (a Proxy whose
|
|
316
|
+
// ownKeys throws), and it must not take the query with it.
|
|
317
|
+
}
|
|
318
|
+
}
|
package/dist/query/types.d.ts
CHANGED
|
@@ -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. */
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -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/query/utils.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Standalone utility functions and classes used by the query builder.
|
|
5
5
|
*/
|
|
6
6
|
import pg from 'pg';
|
|
7
|
+
import { ValidationError } from '../errors.js';
|
|
7
8
|
import { camelToSnake, localDateTimeKind, snakeToCamel, timeOfDayKind } from '../schema.js';
|
|
8
9
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -1492,3 +1493,58 @@ export function selectOmitExclusiveMessage(table) {
|
|
|
1492
1493
|
return (`[turbine] "select" and "omit" are mutually exclusive (on table "${table}"). ` +
|
|
1493
1494
|
`A select already lists exactly the fields you want.`);
|
|
1494
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* The Prisma pagination aliases, folded into Turbine's own spelling ONCE,
|
|
1498
|
+
* before anything reads them.
|
|
1499
|
+
*
|
|
1500
|
+
* Turbine's names are `limit` / `offset`; Prisma's are `take` / `skip`. `take`
|
|
1501
|
+
* was accepted and `skip` was not, which is the worst of the three possible
|
|
1502
|
+
* states: `{ take: 20, skip: 40 }` is what a Prisma habit writes, it looks
|
|
1503
|
+
* accepted because half of it is, and the query silently returns page one
|
|
1504
|
+
* forever. An unknown key is at least inert on its own; a HALF-recognized pair
|
|
1505
|
+
* changes the answer.
|
|
1506
|
+
*
|
|
1507
|
+
* Folded here rather than read at each site deliberately. `take` used to be
|
|
1508
|
+
* handled by six separate `args?.take ?? args?.limit` reads, one of which is
|
|
1509
|
+
* the SQL-cache FINGERPRINT, so adding `skip` the same way would have meant
|
|
1510
|
+
* teaching six places about it and a miss in the fingerprint is not a missing
|
|
1511
|
+
* feature, it is two different pages sharing one cached statement. Normalizing
|
|
1512
|
+
* up front leaves `limit` / `offset` as the single authority and the aliases
|
|
1513
|
+
* cease to exist below this line.
|
|
1514
|
+
*
|
|
1515
|
+
* Returns the SAME object when neither alias is present, so the common path
|
|
1516
|
+
* allocates nothing.
|
|
1517
|
+
*/
|
|
1518
|
+
export function normalizePagination(args) {
|
|
1519
|
+
if (!args)
|
|
1520
|
+
return args;
|
|
1521
|
+
const a = args;
|
|
1522
|
+
if (a.take === undefined && a.skip === undefined)
|
|
1523
|
+
return args;
|
|
1524
|
+
const out = { ...args };
|
|
1525
|
+
if (a.take !== undefined) {
|
|
1526
|
+
assertAliasAgrees('take', a.take, 'limit', a.limit);
|
|
1527
|
+
out.limit = a.take;
|
|
1528
|
+
delete out.take;
|
|
1529
|
+
}
|
|
1530
|
+
if (a.skip !== undefined) {
|
|
1531
|
+
assertAliasAgrees('skip', a.skip, 'offset', a.offset);
|
|
1532
|
+
out.offset = a.skip;
|
|
1533
|
+
delete out.skip;
|
|
1534
|
+
}
|
|
1535
|
+
return out;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Both spellings of one bound, disagreeing. Refused rather than resolved: the
|
|
1539
|
+
* old `take ?? limit` silently preferred one of the two numbers the caller
|
|
1540
|
+
* wrote, and there is no reading of `{ limit: 10, take: 5 }` that makes one of
|
|
1541
|
+
* them the intended answer. Equal values are accepted, since there is nothing
|
|
1542
|
+
* to choose between.
|
|
1543
|
+
*/
|
|
1544
|
+
function assertAliasAgrees(alias, aliasValue, native, nativeValue) {
|
|
1545
|
+
if (nativeValue === undefined || nativeValue === aliasValue)
|
|
1546
|
+
return;
|
|
1547
|
+
throw new ValidationError(`[turbine] "${alias}" and "${native}" are the same option and were given different values ` +
|
|
1548
|
+
`(${alias}: ${String(aliasValue)}, ${native}: ${String(nativeValue)}). ` +
|
|
1549
|
+
`"${alias}" is Prisma's spelling of "${native}"; pass one of them.`);
|
|
1550
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"!dist/**/*.js.map",
|
|
108
108
|
"!dist/**/examples.*",
|
|
109
109
|
"!dist/**/*.d.ts.map",
|
|
110
|
+
"skills",
|
|
110
111
|
"LICENSE",
|
|
111
112
|
"README.md"
|
|
112
113
|
],
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: turbine-orm
|
|
3
|
+
description: Use when writing or debugging Turbine ORM queries in TypeScript - covers the with clause, WHERE operators, relation filters, aggregates, groupBy having, pagination, JSON paths, and the errors each one throws
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing Turbine queries
|
|
7
|
+
|
|
8
|
+
Turbine is a Postgres-first TypeScript ORM with a Prisma-shaped API. If you know
|
|
9
|
+
Prisma, four differences account for most first-try failures, and they are the
|
|
10
|
+
first four sections here.
|
|
11
|
+
|
|
12
|
+
Every construct below is executed against a live database by
|
|
13
|
+
`evals/src/verify-skill.ts` in the turbine-orm repository, on every release. A
|
|
14
|
+
claim in this file that stops being true fails that check by name.
|
|
15
|
+
|
|
16
|
+
## 1. Relations are `with`, never `include`
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const orders = await db.orders.findMany({
|
|
20
|
+
select: { id: true, total: true },
|
|
21
|
+
with: { customer: { select: { email: true } } },
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`include` is Prisma's word. Turbine ignores an unrecognized option, so an
|
|
26
|
+
`include` runs, returns rows, and the relation is simply absent from every one
|
|
27
|
+
of them. Since 0.73.0 it also prints a dev-mode warning naming `with`; in
|
|
28
|
+
production it is silent.
|
|
29
|
+
|
|
30
|
+
## 2. Relation names are derived, and not from the column
|
|
31
|
+
|
|
32
|
+
A relation name is not the foreign-key column, and not always the table name:
|
|
33
|
+
|
|
34
|
+
- **belongsTo** (the side holding the FK) is the target table, singularised and
|
|
35
|
+
camelCased. `guild_id` on `cheese_wheels` gives the relation `guild`.
|
|
36
|
+
- **hasMany** (the side pointed at) is the child table, camelCased and left
|
|
37
|
+
plural: `cheese_wheels` gives `cheeseWheels`.
|
|
38
|
+
- **manyToMany** across a junction is named for the FAR table, not the junction.
|
|
39
|
+
With `wheel_cultures` joining `cheese_wheels` and `cultures`, the relation on
|
|
40
|
+
`cheese_wheels` is `cultures`. The junction is *also* exposed as a plain
|
|
41
|
+
hasMany (`wheelCultures`), and picking that one gives junction rows rather
|
|
42
|
+
than the entities you wanted.
|
|
43
|
+
|
|
44
|
+
If a `turbine mcp` server is connected, `relation_graph` lists these names
|
|
45
|
+
exactly, and `find_join_path` returns the `with` clause to write. Read them
|
|
46
|
+
rather than deriving them.
|
|
47
|
+
|
|
48
|
+
## 3. Either spelling of a name works, and results are always camelCase
|
|
49
|
+
|
|
50
|
+
A column or relation may be written in the schema's `snake_case` or in the
|
|
51
|
+
generated `camelCase`, in every argument position, and the two produce identical
|
|
52
|
+
SQL. Results are always camelCase: `cave_humidity_pct` comes back as
|
|
53
|
+
`caveHumidityPct`.
|
|
54
|
+
|
|
55
|
+
Prefer camelCase, because that is what the generated types autocomplete and what
|
|
56
|
+
you will read back. A name that is neither spelling is a `ValidationError`
|
|
57
|
+
(`TURBINE_E003`) for a column, `RelationError` (`TURBINE_E005`) for a relation,
|
|
58
|
+
and the message suggests the closest real name.
|
|
59
|
+
|
|
60
|
+
## 4. `select` is columns only
|
|
61
|
+
|
|
62
|
+
`select` and `omit` name columns. Naming a relation in `select` throws
|
|
63
|
+
`TURBINE_E003` pointing at `with`; relations carry their own nested `select`.
|
|
64
|
+
`select` and `omit` together throw, and so does a `select` that names no field.
|
|
65
|
+
|
|
66
|
+
## Reads
|
|
67
|
+
|
|
68
|
+
| method | returns |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `findMany` | `T[]` |
|
|
71
|
+
| `findFirst` / `findFirstOrThrow` | the first row matching an optional filter |
|
|
72
|
+
| `findUnique` / `findUniqueOrThrow` | one row addressed by a unique key |
|
|
73
|
+
| `count` | a `number` |
|
|
74
|
+
| `aggregate` | one row of aggregates |
|
|
75
|
+
| `groupBy` | one row per group |
|
|
76
|
+
|
|
77
|
+
`findUnique` requires a `where` that identifies a single row: a primary key, a
|
|
78
|
+
single-column unique, or every column of a compound unique. Anything else is
|
|
79
|
+
`TURBINE_E003` naming the keys that would work. Use `findFirst` for "any row
|
|
80
|
+
matching a filter", and give it an `orderBy` if you care which one.
|
|
81
|
+
|
|
82
|
+
## WHERE
|
|
83
|
+
|
|
84
|
+
A bare value means equality; `null` means `IS NULL`.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
where: {
|
|
88
|
+
status: 'graded',
|
|
89
|
+
retiredAt: null,
|
|
90
|
+
caveHumidityPct: { gte: 90, lte: 94 },
|
|
91
|
+
rindStyle: { in: ['washed', 'waxed'] },
|
|
92
|
+
givenName: { contains: 'Roux', mode: 'insensitive' },
|
|
93
|
+
batchRef: { startsWith: 'WB-' },
|
|
94
|
+
wheelCount: { not: 0 },
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Operators: `equals`, `not`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`,
|
|
99
|
+
`startsWith`, `endsWith`, plus `mode: 'insensitive'`. `AND`, `OR` and `NOT` nest
|
|
100
|
+
at any depth; `AND` and `OR` take arrays.
|
|
101
|
+
|
|
102
|
+
Values are always bound as parameters, and `contains` / `startsWith` /
|
|
103
|
+
`endsWith` escape LIKE wildcards for you. There is no code path in the typed API
|
|
104
|
+
that concatenates a value into SQL.
|
|
105
|
+
|
|
106
|
+
## Relation filters: `some` / `none` / `every`
|
|
107
|
+
|
|
108
|
+
Filter parents by their children without returning the children:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
where: { cheeseWheels: { some: { status: 'quarantined' } } }
|
|
112
|
+
where: { ripeningChecks: { none: {} } } // has no rows at all
|
|
113
|
+
where: { cheeseWheels: { every: { caveHumidityPct: { gte: 85 } } } }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
These are filters, not projections: they add nothing to the result. To get the
|
|
117
|
+
children too, add a `with`.
|
|
118
|
+
|
|
119
|
+
## Per-relation options
|
|
120
|
+
|
|
121
|
+
Everything inside a `with` applies PER PARENT ROW, so `limit: 3` means three
|
|
122
|
+
children each, not three overall.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
with: {
|
|
126
|
+
affineurs: {
|
|
127
|
+
select: { id: true },
|
|
128
|
+
where: { isJourneyman: true },
|
|
129
|
+
orderBy: { id: 'desc' },
|
|
130
|
+
limit: 3,
|
|
131
|
+
with: { cheeseWheels: { select: { id: true } } }, // nests to any depth
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Ordering
|
|
137
|
+
|
|
138
|
+
`orderBy` takes an object or an array of objects; an array keeps its order as
|
|
139
|
+
tie breakers. Two special forms:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
orderBy: [{ pressedOn: 'desc' }, { id: 'desc' }]
|
|
143
|
+
orderBy: { affineurs: { _count: 'desc' } }
|
|
144
|
+
orderBy: { tastingNotes: { path: ['panel', 'score'], direction: 'desc' } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Without an `orderBy`, row order is undefined. A paginated query with no
|
|
148
|
+
`orderBy` is not just unordered, it is unstable: the same row can appear on two
|
|
149
|
+
pages or on none. Turbine warns about it in development.
|
|
150
|
+
|
|
151
|
+
## Pagination
|
|
152
|
+
|
|
153
|
+
`limit` and `offset`, with Prisma's `take` and `skip` accepted as aliases for
|
|
154
|
+
them. Passing both spellings of one bound with different values throws.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
{ limit: 20, offset: 40 } // the same query, written two ways
|
|
158
|
+
{ take: 20, skip: 40 }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For deep pages, prefer a `cursor` over a large `offset`.
|
|
162
|
+
|
|
163
|
+
## JSON columns
|
|
164
|
+
|
|
165
|
+
A `path` array walks the document, and combines with the normal operators:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
where: { tastingNotes: { path: ['panel', 'score'], gte: 9 } }
|
|
169
|
+
where: { credentialBlob: { path: ['tier'], equals: 'master' } }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A path takes a NARROWER operator set than a plain column: `equals`, `gt`, `gte`,
|
|
173
|
+
`lt`, `lte`, `contains`, `startsWith`, `endsWith`, plus `mode`. `not`, `in` and
|
|
174
|
+
`notIn` are refused with `TURBINE_E003` listing what is accepted; express them
|
|
175
|
+
with `NOT` / `OR` around the path filter instead.
|
|
176
|
+
|
|
177
|
+
`_count` accepts a JSON path, and `groupBy` accepts one as a grouping key via
|
|
178
|
+
`{ field, path }`. **`_avg`, `_sum`, `_min` and `_max` do not work on a JSON
|
|
179
|
+
path**: they reach the database as `avg(jsonb)` and fail there. Cast the value
|
|
180
|
+
into a real column, or aggregate in application code.
|
|
181
|
+
|
|
182
|
+
## Aggregates
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await db.ripeningChecks.aggregate({
|
|
186
|
+
where: { rindScore: { gte: 8 } },
|
|
187
|
+
_avg: { aromaScore: true },
|
|
188
|
+
_max: { aromaScore: true },
|
|
189
|
+
_count: { id: true },
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## groupBy and having
|
|
194
|
+
|
|
195
|
+
`by` lists the grouping columns. The `having` shape is **column first, aggregate
|
|
196
|
+
second**, which is the opposite of how it reads aloud:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await db.cheeseWheels.groupBy({
|
|
200
|
+
by: ['rindStyle'],
|
|
201
|
+
_count: { id: true },
|
|
202
|
+
having: { id: { _count: { gt: 55 } } },
|
|
203
|
+
orderBy: { rindStyle: 'asc' },
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`having: { wheelCount: { _sum: { gt: 400 } } }` filters on a summed column. The
|
|
208
|
+
column named in `having` must be one you grouped by or aggregated.
|
|
209
|
+
|
|
210
|
+
## Unique lookups
|
|
211
|
+
|
|
212
|
+
For a single-column unique, name the column. For a compound unique, use the
|
|
213
|
+
joined selector whose value is an object of the parts, or pass the columns flat:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
where: { guildId_batchRef: { guildId: 4, batchRef: 'WB-0007' } }
|
|
217
|
+
where: { guildId: 4, batchRef: 'WB-0007' } // equivalent
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## `distinct`
|
|
221
|
+
|
|
222
|
+
`distinct: ['status']` de-duplicates on those columns.
|
|
223
|
+
|
|
224
|
+
## Errors worth branching on
|
|
225
|
+
|
|
226
|
+
Every error extends `TurbineError` and carries a stable `code`. The ones a query
|
|
227
|
+
produces:
|
|
228
|
+
|
|
229
|
+
| code | class | means |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `TURBINE_E003` | `ValidationError` | unknown column, bad operator, refused shape |
|
|
232
|
+
| `TURBINE_E005` | `RelationError` | unknown relation name in `with` |
|
|
233
|
+
| `TURBINE_E001` | `NotFoundError` | an `*OrThrow` matched nothing |
|
|
234
|
+
| `TURBINE_E008` | `UniqueConstraintError` | a write hit a unique constraint |
|
|
235
|
+
|
|
236
|
+
Branch on the class or the code, never on the message text: messages are not
|
|
237
|
+
covered by semver, and every message carries a link to its docs page.
|
|
238
|
+
|
|
239
|
+
## Before you answer
|
|
240
|
+
|
|
241
|
+
- Did the task ask for a relation? Then it is `with`, not `include`, and not
|
|
242
|
+
`select`.
|
|
243
|
+
- Did it name exact columns? Then `select` exactly those and no more.
|
|
244
|
+
- Did it ask for an order? Then `orderBy` it explicitly.
|
|
245
|
+
- Did it ask for a count rather than rows? Then `count`, not `findMany`.
|
|
246
|
+
- Are you looking a row up by something that is not a unique key? Then
|
|
247
|
+
`findFirst`, not `findUnique`.
|