@vantreeseba/drizzle-graphql 15.0.0 → 15.1.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 +100 -66
- package/dist/index.cjs +112 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +112 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
- Pass your drizzle database instance
|
|
9
|
+
- Pass your drizzle database instance into the builder to generate a `{ schema, entities }` object. The tables are read from the `relations` you gave the drizzle constructor (`drizzle({ client, relations })`), so a table missing from your relations config is not part of the generated schema
|
|
10
10
|
- Use `schema` if pre-built schema already satisfies all your needs. It's compatible with any server that consumes `GraphQLSchema` class instance
|
|
11
11
|
|
|
12
12
|
Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
|
@@ -39,6 +39,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
39
39
|
|
|
40
40
|
// Schema contains 'Users' and 'Customers' tables
|
|
41
41
|
import { db } from './database'
|
|
42
|
+
import { users } from './schema'
|
|
42
43
|
|
|
43
44
|
const { entities } = buildSchema(db)
|
|
44
45
|
|
|
@@ -63,7 +64,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
63
64
|
},
|
|
64
65
|
resolve: async (source, args, context, info) => {
|
|
65
66
|
// Your custom logic goes here...
|
|
66
|
-
const result = await db.select(
|
|
67
|
+
const result = await db.select().from(users).where(/* … */)
|
|
67
68
|
|
|
68
69
|
return result
|
|
69
70
|
}
|
|
@@ -168,8 +169,9 @@ const { schema } = buildSchema(db, {
|
|
|
168
169
|
turns the group-by queries off with it
|
|
169
170
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
170
171
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
171
|
-
- Turning off
|
|
172
|
-
`mutations: false`
|
|
172
|
+
- Turning off every mutation feature omits the `Mutation` type entirely, the same as
|
|
173
|
+
`mutations: false` — unless a table has `softDelete`, whose `restore` mutations are
|
|
174
|
+
generated regardless
|
|
173
175
|
- List and single queries are always generated, so `Query` is never empty
|
|
174
176
|
|
|
175
177
|
### Per-table operations
|
|
@@ -344,9 +346,7 @@ Together the two close the loop: `contextValues` decides which tenant a new row
|
|
|
344
346
|
|
|
345
347
|
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
346
348
|
build instead of quietly ceasing to be scoped.
|
|
347
|
-
|
|
348
|
-
> or an equivalent layer above the schema, which has the request context this library
|
|
349
|
-
> does not.
|
|
349
|
+
|
|
350
350
|
## Naming
|
|
351
351
|
|
|
352
352
|
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
@@ -463,7 +463,7 @@ Keyed by the **Drizzle schema key**, like `scope` and `exclude`. A function form
|
|
|
463
463
|
accepted, for a convention applied across the schema:
|
|
464
464
|
|
|
465
465
|
```Typescript
|
|
466
|
-
softDelete: (table, tableName) => ('deletedAt' in
|
|
466
|
+
softDelete: (table, tableName) => ('deletedAt' in getColumns(table) ? 'deletedAt' : undefined)
|
|
467
467
|
```
|
|
468
468
|
|
|
469
469
|
What a declared table gets:
|
|
@@ -612,6 +612,7 @@ generated SDL says what a field actually holds instead of falling back to `Strin
|
|
|
612
612
|
| `timestamp` / `datetime` (either `mode`) | `DateTime` | an ISO-8601 string |
|
|
613
613
|
| SQLite `integer({ mode: 'timestamp' })` / `'timestamp_ms'` | `DateTime` | an ISO-8601 string |
|
|
614
614
|
| `date` (either `mode`) | `Date` | a `YYYY-MM-DD` string |
|
|
615
|
+
| `numeric` / `decimal` | `Decimal` | a decimal string |
|
|
615
616
|
|
|
616
617
|
```graphql
|
|
617
618
|
mutation {
|
|
@@ -629,6 +630,8 @@ mutation {
|
|
|
629
630
|
`Number.MAX_SAFE_INTEGER` survive the round-trip. Integer literals are accepted on input;
|
|
630
631
|
floats and non-numeric strings are rejected
|
|
631
632
|
- **`UUID`** validates the format on the way in, including inside `where` filters
|
|
633
|
+
- **`Decimal`** is a decimal string in both directions, so the precision the column declares
|
|
634
|
+
is not lost to a JS float. Non-numeric and non-finite input is rejected
|
|
632
635
|
- **`Date`** is a calendar day — no time, no offset — and is the same type in every position
|
|
633
636
|
a `date` column appears in: the object field, the create and update inputs, the
|
|
634
637
|
`DateFilter` operands, aggregate `min` / `max` and group keys. It takes and returns an
|
|
@@ -656,7 +659,7 @@ mutation {
|
|
|
656
659
|
The scalars are exported if you need them in a hand-written schema:
|
|
657
660
|
|
|
658
661
|
```Typescript
|
|
659
|
-
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
662
|
+
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLDecimalString, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
660
663
|
```
|
|
661
664
|
|
|
662
665
|
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
@@ -781,7 +784,7 @@ only has to handle the cases it cares about:
|
|
|
781
784
|
```Typescript
|
|
782
785
|
const { schema } = buildSchema(db, {
|
|
783
786
|
// `schema` is the Postgres schema the enum was declared in, if any; `values` is its
|
|
784
|
-
// value list.
|
|
787
|
+
// value list. `enumName` and `schema` are undefined for an inline value list.
|
|
785
788
|
enumNameMapper: ({ enumName, schema }) =>
|
|
786
789
|
schema === 'reporting' ? `Reporting${enumName}Enum` : undefined,
|
|
787
790
|
})
|
|
@@ -861,9 +864,40 @@ const { schema } = buildSchema(db, {
|
|
|
861
864
|
|
|
862
865
|
- `describeColumn` reaches every place the column appears: the object type, the create and
|
|
863
866
|
update inputs, the filter input's field, `Min`/`Max` aggregates and `GroupKeys`
|
|
864
|
-
-
|
|
865
|
-
|
|
866
|
-
straight off
|
|
867
|
+
- `describeColumn` receives the drizzle column itself and `describeTable` the drizzle table
|
|
868
|
+
itself, so a project that keeps its documentation in the schema (a `WeakMap` keyed by
|
|
869
|
+
column/table, a custom property, a comment table) can read it straight off:
|
|
870
|
+
|
|
871
|
+
```Typescript
|
|
872
|
+
import type { Table } from 'drizzle-orm';
|
|
873
|
+
|
|
874
|
+
const docs = new WeakMap<object, string>();
|
|
875
|
+
|
|
876
|
+
// Register docs on the *built* table and its columns. `text('email')` is only a builder:
|
|
877
|
+
// pgTable creates a new column object from it, so keying on the builder never matches.
|
|
878
|
+
const described = <T extends Table>(
|
|
879
|
+
table: T,
|
|
880
|
+
description: string,
|
|
881
|
+
columns: Partial<Record<keyof T['_']['columns'], string>> = {},
|
|
882
|
+
): T => {
|
|
883
|
+
docs.set(table, description);
|
|
884
|
+
for (const [key, columnText] of Object.entries(columns)) {
|
|
885
|
+
docs.set((table as any)[key], columnText as string);
|
|
886
|
+
}
|
|
887
|
+
return table;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export const users = described(
|
|
891
|
+
pgTable('users', { id: integer('id').primaryKey(), email: text('email').notNull() }),
|
|
892
|
+
'A person who can sign in',
|
|
893
|
+
{ email: 'Primary login address' },
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
buildSchema(db, {
|
|
897
|
+
describeColumn: (column) => docs.get(column),
|
|
898
|
+
describeTable: (_tableName, table) => docs.get(table),
|
|
899
|
+
});
|
|
900
|
+
```
|
|
867
901
|
- `deprecateColumn` marks output fields and *optional* input fields. A required input field
|
|
868
902
|
is skipped: GraphQL forbids deprecating one, and emitting it would make the schema invalid
|
|
869
903
|
- Filter and `orderBy` fields are never deprecated — filtering on a deprecated column is how
|
|
@@ -1090,7 +1124,8 @@ that column's `inArray` list instead of by the column's value:
|
|
|
1090
1124
|
## Filtering inside JSON documents
|
|
1091
1125
|
|
|
1092
1126
|
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
1093
|
-
document, alongside the whole-document `eq` / `ne`
|
|
1127
|
+
document, alongside the whole-document `eq` / `ne` operators and, on PostgreSQL and MySQL,
|
|
1128
|
+
structural `contains` (SQLite stores JSON as text and has no containment operator):
|
|
1094
1129
|
|
|
1095
1130
|
```graphql
|
|
1096
1131
|
{
|
|
@@ -1256,9 +1291,10 @@ columns you group by.
|
|
|
1256
1291
|
}
|
|
1257
1292
|
```
|
|
1258
1293
|
|
|
1259
|
-
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max`
|
|
1260
|
-
|
|
1261
|
-
|
|
1294
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` take one
|
|
1295
|
+
filter per numeric column (the comparison is numeric, so a `max` over a text or date
|
|
1296
|
+
column has no `having` entry even though the aggregate type returns it),
|
|
1297
|
+
`countNonNull` one per column, and `countDistinct` one per column `countDistinct` exposes
|
|
1262
1298
|
- Every entry in a `having` is ANDed together
|
|
1263
1299
|
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
1264
1300
|
whether or not you asked for `count`
|
|
@@ -1297,6 +1333,8 @@ can count or summarise related rows without fetching them:
|
|
|
1297
1333
|
- A parent with no related rows gets `count: 0` and `null` for every other aggregate
|
|
1298
1334
|
- To-one relations get no aggregate field — there is nothing to aggregate over
|
|
1299
1335
|
- The field is skipped if its name would collide with a column or another relation
|
|
1336
|
+
- Set `features: { relationAggregates: false }` (globally or per table) to leave these
|
|
1337
|
+
fields out
|
|
1300
1338
|
|
|
1301
1339
|
All parents in a selection are aggregated with a single
|
|
1302
1340
|
`SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
|
|
@@ -1320,7 +1358,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
|
|
|
1320
1358
|
```
|
|
1321
1359
|
|
|
1322
1360
|
- Which row survives each group is decided by `orderBy` — the first one wins. With no
|
|
1323
|
-
`orderBy`,
|
|
1361
|
+
`orderBy`, the table's [default ordering](#default-ordering) decides, and failing that the
|
|
1362
|
+
lowest primary key
|
|
1324
1363
|
- `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
|
|
1325
1364
|
**after**, so `limit: 10` returns ten distinct rows
|
|
1326
1365
|
- Several columns are treated as one combined key, not as independent ones
|
|
@@ -1339,7 +1378,7 @@ or **SQLite 3.25+**), and a table with no primary key cannot use it.
|
|
|
1339
1378
|
SQL gives no ordering guarantee for a query that has no `ORDER BY`, so paging through an
|
|
1340
1379
|
unordered result can return the same row twice or skip one entirely. To keep pages stable,
|
|
1341
1380
|
a query that returns only part of a table is ordered by its primary key when the request
|
|
1342
|
-
supplies no `orderBy
|
|
1381
|
+
supplies no `orderBy` and the table has no [default ordering](#default-ordering):
|
|
1343
1382
|
|
|
1344
1383
|
- list queries with `limit` and/or `offset`
|
|
1345
1384
|
- `<tableName>Single` queries, which are an implicit `limit 1`
|
|
@@ -1347,7 +1386,8 @@ supplies no `orderBy`:
|
|
|
1347
1386
|
`orderBy` you do supply, so per-parent slices are deterministic)
|
|
1348
1387
|
|
|
1349
1388
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
1350
|
-
key column, and an unpaginated list query
|
|
1389
|
+
key column, and an unpaginated list query of a table with no `defaults` entry is left
|
|
1390
|
+
unordered so no sort is paid for.
|
|
1351
1391
|
|
|
1352
1392
|
## Default ordering
|
|
1353
1393
|
|
|
@@ -1434,14 +1474,16 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1434
1474
|
window
|
|
1435
1475
|
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
1436
1476
|
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
1437
|
-
- `NULL`s in ordered columns
|
|
1438
|
-
|
|
1439
|
-
`asc`)
|
|
1440
|
-
through, not skipped
|
|
1477
|
+
- `NULL`s in ordered columns sort where the entry's `nulls: first | last` says or, without
|
|
1478
|
+
it, at each dialect's native `ORDER BY` placement — PostgreSQL sorts them largest (last
|
|
1479
|
+
in `asc`), MySQL and SQLite smallest (first in `asc`). The predicate matches either way,
|
|
1480
|
+
so rows with `NULL` in an ordered column are paged through, not skipped, and `nulls` is
|
|
1481
|
+
part of the ordering a cursor is tied to
|
|
1441
1482
|
- Dates and bigints round-trip losslessly through the cursor
|
|
1442
1483
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1443
1484
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1444
|
-
- `after` cannot be combined with `distinct
|
|
1485
|
+
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
1486
|
+
that orders through a relation (`cursor` resolves to `null` there), and a table with
|
|
1445
1487
|
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
1446
1488
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
1447
1489
|
skipped
|
|
@@ -1595,8 +1637,10 @@ mutation {
|
|
|
1595
1637
|
}
|
|
1596
1638
|
```
|
|
1597
1639
|
|
|
1598
|
-
- `connect`, `disconnect` and `set` take the target's own `Filters` input
|
|
1599
|
-
|
|
1640
|
+
- `connect`, `disconnect` and `set` take the target's own `Filters` input — a list of them
|
|
1641
|
+
on a to-many relation, a single one that must match exactly one row on a to-one — so they
|
|
1642
|
+
select rows the same way a query does. A to-one `disconnect` is just `disconnect: true`,
|
|
1643
|
+
since there is only one row to detach. A filter that matches everything is rejected rather than
|
|
1600
1644
|
attaching the whole table
|
|
1601
1645
|
- `set` replaces the relation wholesale — everything attached is detached first, then the
|
|
1602
1646
|
filters attach. It runs before `create`, so a row created in the same write survives it
|
|
@@ -1944,8 +1988,8 @@ const yoga = createYoga({ schema, plugins: armor.protect().plugins });
|
|
|
1944
1988
|
|
|
1945
1989
|
Armor's `costLimit` is its own document-shape estimate and does not read the `complexity`
|
|
1946
1990
|
extensions above; the two compose, if you want a cheap static ceiling alongside row-count
|
|
1947
|
-
aware pricing. Neither one bounds an individual `limit` argument
|
|
1948
|
-
|
|
1991
|
+
aware pricing. Neither one bounds an individual `limit` argument — a list query with no
|
|
1992
|
+
`limit` is still a full table scan unless you set a [`limits`](#bounding-page-size) policy.
|
|
1949
1993
|
|
|
1950
1994
|
## Authorization
|
|
1951
1995
|
|
|
@@ -1987,21 +2031,17 @@ request context actually lives.
|
|
|
1987
2031
|
or through your own resolver wrapper, and let the ability answer the questions.
|
|
1988
2032
|
|
|
1989
2033
|
What neither can do is **narrow** a query. They permit or deny a field; they do not append
|
|
1990
|
-
`WHERE
|
|
1991
|
-
returns every tenant's rows.
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
wrapped;
|
|
1998
|
-
- put the scoped tables behind database row-level security and give the driver the
|
|
1999
|
-
request's identity, so the narrowing happens below Drizzle entirely.
|
|
2034
|
+
`WHERE org_id = …` to the SQL a generated resolver runs, so a permitted `posts` query still
|
|
2035
|
+
returns every tenant's rows. That is what [`scope`](#row-scoping-and-multi-tenancy) is for: a
|
|
2036
|
+
per-table predicate built from the request context and ANDed onto every read and write of
|
|
2037
|
+
that table, nested relation fields included. Pair it with
|
|
2038
|
+
[`contextValues`](#columns-the-server-owns) so new rows are stamped with the caller's tenant.
|
|
2039
|
+
The usual split is shield (or CASL) to decide who may reach a field, and `scope` to decide
|
|
2040
|
+
which rows they see through it.
|
|
2000
2041
|
|
|
2001
2042
|
None of this removes anything from introspection either — `deny` hides the data, not the
|
|
2002
|
-
shape. If the schema must not so much as mention a table,
|
|
2003
|
-
|
|
2004
|
-
cheap).
|
|
2043
|
+
shape. If the schema must not so much as mention a table or column, leave it out with
|
|
2044
|
+
[`exclude`](#excluding-tables-and-columns).
|
|
2005
2045
|
|
|
2006
2046
|
Nor is it a row bound: a complexity ceiling prices the `limit` a client asks for, it does not
|
|
2007
2047
|
cap it. Use [`limits`](#bounding-page-size) for that.
|
|
@@ -2142,6 +2182,9 @@ are untouched.
|
|
|
2142
2182
|
- A document that mixes in mutation fields this build did not generate is left alone: their
|
|
2143
2183
|
completion cannot be tracked, so no transaction is opened
|
|
2144
2184
|
- It never nests — an executor supplied on the context wins, as below
|
|
2185
|
+
- A single mutation field can still run in a transaction of its own when it carries a
|
|
2186
|
+
nested write or its table has an `onWrite` hook — those open one regardless of this
|
|
2187
|
+
setting
|
|
2145
2188
|
|
|
2146
2189
|
### Supplying your own executor
|
|
2147
2190
|
|
|
@@ -2176,17 +2219,6 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
|
|
|
2176
2219
|
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
2177
2220
|
both end up loaded in one process.
|
|
2178
2221
|
|
|
2179
|
-
Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
|
|
2180
|
-
more than one generated mutation in a single transaction, committed when the last one
|
|
2181
|
-
completes and rolled back if any of them fails.
|
|
2182
|
-
|
|
2183
|
-
```Typescript
|
|
2184
|
-
buildSchema(db, { transactions: 'auto' })
|
|
2185
|
-
```
|
|
2186
|
-
|
|
2187
|
-
A document with one mutation, or one that selects a mutation field this build did not
|
|
2188
|
-
generate, still runs unwrapped — there is nothing to keep atomic.
|
|
2189
|
-
|
|
2190
2222
|
## Write hooks
|
|
2191
2223
|
|
|
2192
2224
|
`onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
|
|
@@ -2347,12 +2379,12 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2347
2379
|
relation in a single round-trip using Drizzle's relational query builder
|
|
2348
2380
|
(`with:`), including nested relations and per-relation `where` / `orderBy` /
|
|
2349
2381
|
`limit` / `offset` arguments.
|
|
2350
|
-
- **Mutations (PostgreSQL & SQLite)** — after an insert
|
|
2382
|
+
- **Mutations (PostgreSQL & SQLite)** — after an insert, upsert, update or `updateMany`, if the selection set
|
|
2351
2383
|
includes relation fields, the affected rows are re-fetched by primary key through one
|
|
2352
2384
|
relational query so their relations are eagerly loaded (single- and composite-column
|
|
2353
|
-
primary keys are supported). `delete` mutations keep using the
|
|
2354
|
-
|
|
2355
|
-
|
|
2385
|
+
primary keys are supported). `delete` and `restore` mutations keep using the
|
|
2386
|
+
request-scoped batch loader. MySQL mutations return only a success flag (no row
|
|
2387
|
+
payload), so this step does not apply there.
|
|
2356
2388
|
- **Custom schemas** — each relation field also has a standalone resolver, exported as
|
|
2357
2389
|
`entities.fieldResolvers[TableName][relationName]`. When you wire generated types
|
|
2358
2390
|
into your own schema and your root resolver does **not** pre-fetch relations, these
|
|
@@ -2369,9 +2401,8 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2369
2401
|
|
|
2370
2402
|
> [!IMPORTANT]
|
|
2371
2403
|
> Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
|
|
2372
|
-
> rely on SQL window functions. These require **PostgreSQL**,
|
|
2373
|
-
> **SQLite 3.25+**.
|
|
2374
|
-
> have no such requirement.
|
|
2404
|
+
> and the `distinct` argument rely on SQL window functions. These require **PostgreSQL**,
|
|
2405
|
+
> **MySQL 8.0+**, or **SQLite 3.25+**. Everything else has no such requirement.
|
|
2375
2406
|
|
|
2376
2407
|
### Relation nullability
|
|
2377
2408
|
|
|
@@ -2432,7 +2463,9 @@ const finalSchema = addResolversToSchema({
|
|
|
2432
2463
|
excludes that relation from `with:` (and from the mutation eager re-fetch).
|
|
2433
2464
|
|
|
2434
2465
|
Opting a relation out does **not** remove its field — it keeps resolving lazily via the
|
|
2435
|
-
request-scoped batch loader, so the field still works even before you override it.
|
|
2466
|
+
request-scoped batch loader, so the field still works even before you override it. A
|
|
2467
|
+
many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
|
|
2468
|
+
lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
|
|
2436
2469
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
2437
2470
|
of `entities.fieldResolvers`.
|
|
2438
2471
|
|
|
@@ -2475,9 +2508,10 @@ into its nested entry, and leaves `<relation>Aggregate` selections out — they
|
|
|
2475
2508
|
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
2476
2509
|
and returns the rows already remapped to their GraphQL transport forms.
|
|
2477
2510
|
|
|
2478
|
-
Both take
|
|
2479
|
-
|
|
2480
|
-
that renames its types has to say so.
|
|
2511
|
+
Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
|
|
2512
|
+
`typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
|
|
2513
|
+
published GraphQL type names, so a build that renames its types has to say so here too. If
|
|
2514
|
+
the build used `typeNameMapper: 'singularize'`, pass the exported `singularizeMapper`.
|
|
2481
2515
|
|
|
2482
2516
|
## Identifying generated fields
|
|
2483
2517
|
|
|
@@ -2548,7 +2582,7 @@ that explains it, where there is one.
|
|
|
2548
2582
|
| `mutations` | `true` | `false` omits the `Mutation` type entirely |
|
|
2549
2583
|
| `features` | see [Choosing what gets generated](#choosing-what-gets-generated) | which operations are generated, per operation |
|
|
2550
2584
|
| `exclude` | none | [tables and columns left out](#excluding-tables-and-columns) of the generated schema entirely |
|
|
2551
|
-
| `relationsDepthLimit` | unlimited |
|
|
2585
|
+
| `relationsDepthLimit` | unlimited | `0` generates no relation fields (a flat, columns-only schema); any other value currently behaves like the default |
|
|
2552
2586
|
| `prefixes` / `suffixes` / `typeNameMapper` | see [Naming](#naming) | operation and type names |
|
|
2553
2587
|
| `typeNamePrefix` / `typeNameSuffix` / `derivedTypeNameMapper` | none | [names of the derived types](#renaming-the-derived-types) — filters, inputs, aggregates, shared inputs |
|
|
2554
2588
|
| `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
|
|
@@ -2556,7 +2590,7 @@ that explains it, where there is one.
|
|
|
2556
2590
|
| `enumValueNameMapper` | the stored value | [names the enum's values](#enum-value-names); `'constant'` for CONSTANT_CASE |
|
|
2557
2591
|
| `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
|
|
2558
2592
|
| `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
|
|
2559
|
-
| `conflictDoNothing` | `false` | PostgreSQL
|
|
2593
|
+
| `conflictDoNothing` | `false` | **Deprecated** — use [`onConflict: { action: NOTHING }`](#upsert) per request. PostgreSQL and SQLite: `ON CONFLICT DO NOTHING` on every insert, which makes `create<Table>Single` nullable; ignored on MySQL |
|
|
2560
2594
|
| `complexity` | on | [cost hints](#query-cost) in field extensions; `false` turns them off |
|
|
2561
2595
|
| `transactions` | `'none'` | `'auto'` wraps a multi-mutation request in one [transaction](#transactions) |
|
|
2562
2596
|
| `limits` | unbounded | [default and maximum page size](#bounding-page-size), globally or per table |
|
package/dist/index.cjs
CHANGED
|
@@ -1813,6 +1813,48 @@ const extractRelationJoinColumns = (relEntry, parentTable, targetTable) => {
|
|
|
1813
1813
|
};
|
|
1814
1814
|
};
|
|
1815
1815
|
/**
|
|
1816
|
+
* The junction table and its two columns for a `.through()` (many-to-many) relation:
|
|
1817
|
+
* `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
|
|
1818
|
+
* with no junction; `null` for one whose junction cannot be used here — more than one column
|
|
1819
|
+
* on either side, or columns that are not on the junction table.
|
|
1820
|
+
*/
|
|
1821
|
+
const extractJunctionColumns = (relEntry) => {
|
|
1822
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1823
|
+
const junction = rel?.throughTable;
|
|
1824
|
+
if (!junction) return;
|
|
1825
|
+
const sourceEntries = rel.through?.source;
|
|
1826
|
+
const targetEntries = rel.through?.target;
|
|
1827
|
+
if (sourceEntries?.length !== 1 || targetEntries?.length !== 1) return null;
|
|
1828
|
+
const junctionCols = Object.values((0, drizzle_orm.getColumns)(junction));
|
|
1829
|
+
const sourceCol = unwrapRelationColumn(sourceEntries[0]);
|
|
1830
|
+
const targetCol = unwrapRelationColumn(targetEntries[0]);
|
|
1831
|
+
if (!sourceCol || !targetCol || !junctionCols.includes(sourceCol) || !junctionCols.includes(targetCol)) return null;
|
|
1832
|
+
return {
|
|
1833
|
+
table: junction,
|
|
1834
|
+
sourceCol,
|
|
1835
|
+
targetCol
|
|
1836
|
+
};
|
|
1837
|
+
};
|
|
1838
|
+
/**
|
|
1839
|
+
* The relation's own declared `where` as a condition on a query that reads the target table,
|
|
1840
|
+
* or `undefined` when it has none. The eager path gets this from drizzle's `with:`; the batched
|
|
1841
|
+
* field resolver and `<relation>Aggregate` read the target table directly, so they have to add
|
|
1842
|
+
* it themselves or they return rows the relation hides.
|
|
1843
|
+
*
|
|
1844
|
+
* A reversed filter (inherited from the relation on the other side) is written against the
|
|
1845
|
+
* parent table, so it is applied as "the row's parent key belongs to a parent that passes it".
|
|
1846
|
+
* `parentKeyCol` is the parent's join column and `groupCol` the column in the query that holds
|
|
1847
|
+
* the parent key for each row — the target's foreign key, or the junction's source column.
|
|
1848
|
+
*/
|
|
1849
|
+
const relationOwnCondition = (relEntry, parentTable, targetTable, parentKeyCol, groupCol) => {
|
|
1850
|
+
const rel = relEntry.relation ?? relEntry;
|
|
1851
|
+
const ownWhere = relationOwnWhere(rel);
|
|
1852
|
+
if (!ownWhere) return;
|
|
1853
|
+
if (!isRelationFilterReversed(rel)) return (0, drizzle_orm.relationsFilterToSQL)(targetTable, ownWhere);
|
|
1854
|
+
const parentFilter = (0, drizzle_orm.relationsFilterToSQL)(parentTable, ownWhere);
|
|
1855
|
+
return drizzle_orm.sql`${groupCol} in (select ${parentKeyCol} from ${parentTable} where ${parentFilter})`;
|
|
1856
|
+
};
|
|
1857
|
+
/**
|
|
1816
1858
|
* Whether a to-one relation can back a relation orderBy hop. `.through()` (junction)
|
|
1817
1859
|
* relations are excluded — the ordering subquery only joins the direct target, never the
|
|
1818
1860
|
* junction table. (Relation *filters* do support `.through()` — this guard is orderBy-only.)
|
|
@@ -2976,6 +3018,11 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2976
3018
|
//#endregion
|
|
2977
3019
|
//#region src/util/builders/common/distinct.ts
|
|
2978
3020
|
/**
|
|
3021
|
+
* Alias the parent key comes back under when the pass runs through a junction table. See
|
|
3022
|
+
* `selectDistinctKeys`' `junction` parameter.
|
|
3023
|
+
*/
|
|
3024
|
+
const DISTINCT_PARENT = "__dgql_distinct_parent";
|
|
3025
|
+
/**
|
|
2979
3026
|
* Keeps the first row of each distinct combination of the requested columns, following the
|
|
2980
3027
|
* query's own ordering, then applies `limit`/`offset` to what survives — and returns the
|
|
2981
3028
|
* surviving rows' primary key values in that order.
|
|
@@ -2988,9 +3035,14 @@ const getUniqueColumnSets = (table, getTableConfig) => {
|
|
|
2988
3035
|
* `partitionBy` prepends extra columns to the window's partition. A to-many relation field
|
|
2989
3036
|
* uses it to pass the foreign key, which makes the pass distinct *within each parent* rather
|
|
2990
3037
|
* than across the whole batch — the same meaning `distinct` has on a root list.
|
|
3038
|
+
*
|
|
3039
|
+
* `junction` is for a many-to-many relation: the target is joined to the junction `on` the
|
|
3040
|
+
* given condition, and each returned key also carries the parent it was kept for, under
|
|
3041
|
+
* {@link DISTINCT_PARENT}. A target row linked to several parents can be kept for one and
|
|
3042
|
+
* dropped for another, so a key alone cannot narrow such a batch.
|
|
2991
3043
|
*/
|
|
2992
3044
|
const selectDistinctKeys = async (params) => {
|
|
2993
|
-
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy } = params;
|
|
3045
|
+
const { db, table, tableName, distinct, pkNames, where, orderBy, limit, offset, partitionBy, junction } = params;
|
|
2994
3046
|
const cols = (0, drizzle_orm.getColumns)(table);
|
|
2995
3047
|
if (!pkNames.length) throw drizzleError(`Table ${tableName} has no primary key, so 'distinct' cannot be applied to it.`, { code: "DRIZZLE_INVALID_DISTINCT" });
|
|
2996
3048
|
const requestedCols = distinct.map((name) => cols[name]).filter(Boolean);
|
|
@@ -2999,12 +3051,17 @@ const selectDistinctKeys = async (params) => {
|
|
|
2999
3051
|
const orderEntries = orderBy ? orderByEntries(orderBy) : [];
|
|
3000
3052
|
const windowOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls, cols[column])), ...primaryKeyOrderExprs(table, pkNames)];
|
|
3001
3053
|
const rowNumber = drizzle_orm.sql`row_number() over (partition by ${drizzle_orm.sql.join(partitionCols, drizzle_orm.sql`, `)} order by ${drizzle_orm.sql.join(windowOrder, drizzle_orm.sql`, `)})`.as(DISTINCT_RN);
|
|
3002
|
-
const
|
|
3054
|
+
const selected = db.select({
|
|
3003
3055
|
...cols,
|
|
3056
|
+
...junction ? { [DISTINCT_PARENT]: junction.parentKey } : {},
|
|
3004
3057
|
[DISTINCT_RN]: rowNumber
|
|
3005
|
-
}).from(table)
|
|
3058
|
+
}).from(table);
|
|
3059
|
+
const sub = (junction ? selected.innerJoin(junction.table, junction.on) : selected).where(where).as("__dgql_distinct");
|
|
3006
3060
|
const outerOrder = [...orderEntries.flatMap(([column, direction, nulls]) => orderExpressions(sub[column], direction, nulls, cols[column])), ...pkNames.filter((name) => sub[name]).map((name) => (0, drizzle_orm.asc)(sub[name]))];
|
|
3007
|
-
let query = db.select(
|
|
3061
|
+
let query = db.select({
|
|
3062
|
+
...Object.fromEntries(pkNames.map((name) => [name, sub[name]])),
|
|
3063
|
+
...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
|
|
3064
|
+
}).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
|
|
3008
3065
|
if (offset) query = query.offset(offset);
|
|
3009
3066
|
if (limit != null) query = query.limit(limit);
|
|
3010
3067
|
return await query;
|
|
@@ -4625,16 +4682,17 @@ const identifyRows = (field, args) => {
|
|
|
4625
4682
|
* Window functions require PostgreSQL, MySQL >= 8.0, or SQLite >= 3.25.
|
|
4626
4683
|
* Returns raw rows (NOT remapped); the caller groups + remaps them.
|
|
4627
4684
|
*/
|
|
4628
|
-
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs) => {
|
|
4685
|
+
const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereCondition, orderByArg, limit, offset, pkNames, columns, orderCtx, whereArgs, junction) => {
|
|
4629
4686
|
const cols = columns ?? (0, drizzle_orm.getColumns)(targetTable);
|
|
4630
4687
|
const orderExprs = [...orderByArg ? extractOrderBy(targetTable, orderByArg, orderCtx, whereArgs) : [], ...primaryKeyOrderExprs(targetTable, pkNames)];
|
|
4631
4688
|
const orderClause = orderExprs.length ? drizzle_orm.sql` order by ${drizzle_orm.sql.join(orderExprs, drizzle_orm.sql`, `)}` : drizzle_orm.sql``;
|
|
4632
4689
|
const RN = "__drizzle_graphql_rn";
|
|
4633
|
-
const
|
|
4634
|
-
const
|
|
4690
|
+
const partitionCol = junction ? junction.sourceCol : foreignCol;
|
|
4691
|
+
const rowNumber = drizzle_orm.sql`row_number() over (partition by ${partitionCol}${orderClause})`.as(RN);
|
|
4692
|
+
const sub = withJunction(db.select({
|
|
4635
4693
|
...cols,
|
|
4636
4694
|
[RN]: rowNumber
|
|
4637
|
-
}).from(targetTable).where(whereCondition).as("__paginated");
|
|
4695
|
+
}).from(targetTable), junction, foreignCol).where(whereCondition).as("__paginated");
|
|
4638
4696
|
const lower = offset ?? 0;
|
|
4639
4697
|
const windowConds = [(0, drizzle_orm.gt)(sub[RN], lower)];
|
|
4640
4698
|
if (limit != null) windowConds.push((0, drizzle_orm.lte)(sub[RN], lower + limit));
|
|
@@ -4643,6 +4701,14 @@ const batchedPaginatedRelationQuery = async (db, targetTable, foreignCol, whereC
|
|
|
4643
4701
|
return rows;
|
|
4644
4702
|
};
|
|
4645
4703
|
/**
|
|
4704
|
+
* Alias the parent key is selected under when a relation goes through a junction table. The
|
|
4705
|
+
* key lives on the junction, not the target, so it has to be selected explicitly to group the
|
|
4706
|
+
* rows by parent — and a target row linked to several parents comes back once per parent.
|
|
4707
|
+
*/
|
|
4708
|
+
const PARENT_KEY = "__drizzle_graphql_parent";
|
|
4709
|
+
/** `from(target)`, joined to the junction table when the relation has one. */
|
|
4710
|
+
const withJunction = (query, junction, foreignCol) => junction ? query.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : query;
|
|
4711
|
+
/**
|
|
4646
4712
|
* Creates a RelationResolverFactory that generates field-level resolvers for each relation.
|
|
4647
4713
|
* Each resolver:
|
|
4648
4714
|
* 1. Returns pre-fetched data if the parent resolver already included it (eager path, zero cost).
|
|
@@ -4664,6 +4730,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4664
4730
|
const joinCols = extractRelationJoinColumns(relEntry, parentTable, targetTable);
|
|
4665
4731
|
if (!joinCols) return;
|
|
4666
4732
|
const { localColPropName, foreignCol, foreignColPropName } = joinCols;
|
|
4733
|
+
const junction = extractJunctionColumns(relEntry);
|
|
4734
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
4735
|
+
const groupKey = junction ? PARENT_KEY : foreignColPropName;
|
|
4736
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
|
|
4667
4737
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
|
|
4668
4738
|
const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
|
|
4669
4739
|
const targetPkNames = relEntry.targetPkNames ?? [];
|
|
@@ -4741,11 +4811,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4741
4811
|
loaderKey: `${tableName}::${relationName}::${argsKey}`
|
|
4742
4812
|
};
|
|
4743
4813
|
});
|
|
4814
|
+
if (junction === null) throw drizzleError(`Relation '${tableName}.${relationName}' goes through a junction with a composite key, which can only be loaded together with its parent. Leave it to eager loading (eagerLoadRelations).`, { code: "DRIZZLE_UNSUPPORTED_RELATION" });
|
|
4744
4815
|
return getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
4745
4816
|
const executor = resolveExecutor(db, context);
|
|
4746
4817
|
const uniqueIds = [...new Set(parentIds)];
|
|
4747
4818
|
const scope = resolveScope(policies, context, filterCtx);
|
|
4748
|
-
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(
|
|
4819
|
+
let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
|
|
4749
4820
|
if (distinct) {
|
|
4750
4821
|
const keys = await selectDistinctKeys({
|
|
4751
4822
|
db: executor,
|
|
@@ -4755,27 +4826,41 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
|
|
|
4755
4826
|
pkNames: targetPkNames,
|
|
4756
4827
|
where: whereCondition,
|
|
4757
4828
|
orderBy: orderByArg,
|
|
4758
|
-
partitionBy: [
|
|
4829
|
+
partitionBy: [groupCol],
|
|
4830
|
+
junction: junction ? {
|
|
4831
|
+
table: junction.table,
|
|
4832
|
+
on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
|
|
4833
|
+
parentKey: groupCol
|
|
4834
|
+
} : void 0
|
|
4759
4835
|
});
|
|
4760
4836
|
if (!keys.length) return parentIds.map(() => isOne ? null : []);
|
|
4761
|
-
whereCondition = (0, drizzle_orm.and)(whereCondition, primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4837
|
+
whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(groupCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
|
|
4762
4838
|
}
|
|
4839
|
+
const selection = junction ? {
|
|
4840
|
+
...columns ?? (0, drizzle_orm.getColumns)(targetTable),
|
|
4841
|
+
[PARENT_KEY]: groupCol
|
|
4842
|
+
} : columns;
|
|
4763
4843
|
let rows;
|
|
4764
|
-
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames,
|
|
4844
|
+
if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
|
|
4765
4845
|
else {
|
|
4766
|
-
let q = (
|
|
4846
|
+
let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
|
|
4767
4847
|
const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
|
|
4768
4848
|
if (orderExprs.length) q = q.orderBy(...orderExprs);
|
|
4769
4849
|
rows = await q;
|
|
4770
4850
|
}
|
|
4771
4851
|
if (cursorEntries) attachRowCursors(rows, cursorEntries);
|
|
4852
|
+
const keyOf = (row) => {
|
|
4853
|
+
const key = String(row[groupKey]);
|
|
4854
|
+
if (junction) delete row[PARENT_KEY];
|
|
4855
|
+
return key;
|
|
4856
|
+
};
|
|
4772
4857
|
if (isOne) {
|
|
4773
|
-
const byKey = new Map(rows.map((row) => [
|
|
4858
|
+
const byKey = new Map(rows.map((row) => [keyOf(row), row]));
|
|
4774
4859
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4775
4860
|
return parentIds.map((id) => byKey.get(String(id)) ?? null);
|
|
4776
4861
|
}
|
|
4777
4862
|
const grouped = new Map(uniqueIds.map((id) => [String(id), []]));
|
|
4778
|
-
for (const row of rows) grouped.get(
|
|
4863
|
+
for (const row of rows) grouped.get(keyOf(row))?.push(row);
|
|
4779
4864
|
remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
|
|
4780
4865
|
return parentIds.map((id) => grouped.get(String(id)) ?? []);
|
|
4781
4866
|
}).load(localValue);
|
|
@@ -5110,7 +5195,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5110
5195
|
});
|
|
5111
5196
|
const shell = new graphql.GraphQLObjectType({
|
|
5112
5197
|
name: typeName,
|
|
5113
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5198
|
+
description: cacheCtx.docs.describeTable?.(tableName, table),
|
|
5114
5199
|
fields: () => ({
|
|
5115
5200
|
...tableFields,
|
|
5116
5201
|
...container.fields
|
|
@@ -5144,7 +5229,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
|
|
|
5144
5229
|
defaultName: resolveTypeName(targetTableName, typeNameMapper),
|
|
5145
5230
|
table: targetTableName
|
|
5146
5231
|
}),
|
|
5147
|
-
description: cacheCtx.docs.describeTable?.(targetTableName),
|
|
5232
|
+
description: cacheCtx.docs.describeTable?.(targetTableName, targetTable),
|
|
5148
5233
|
fields: () => ({
|
|
5149
5234
|
...targetTableFields,
|
|
5150
5235
|
...capturedTargetContainer.fields
|
|
@@ -5295,7 +5380,7 @@ const generateTableTypes = (tableName, tables, relationMap, withReturning, relat
|
|
|
5295
5380
|
defaultName: resolveTypeName(tableName, typeNameMapper),
|
|
5296
5381
|
table: tableName
|
|
5297
5382
|
}),
|
|
5298
|
-
description: cacheCtx.docs.describeTable?.(tableName),
|
|
5383
|
+
description: cacheCtx.docs.describeTable?.(tableName, tables[tableName]),
|
|
5299
5384
|
fields: {
|
|
5300
5385
|
...tableFields,
|
|
5301
5386
|
...relationFields
|
|
@@ -5862,6 +5947,10 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5862
5947
|
return;
|
|
5863
5948
|
}
|
|
5864
5949
|
const { localColPropName, foreignCol } = joinCols;
|
|
5950
|
+
const junction = extractJunctionColumns(relEntry);
|
|
5951
|
+
if (junction === null) return;
|
|
5952
|
+
const groupCol = junction ? junction.sourceCol : foreignCol;
|
|
5953
|
+
const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, (0, drizzle_orm.getColumns)(parentTable)[localColPropName], groupCol);
|
|
5865
5954
|
const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
|
|
5866
5955
|
const targetTypeName = resolveTypeName(targetTableName, typeNameMapper);
|
|
5867
5956
|
const type = generateAggregateTypes(targetTable, targetTableName, targetTypeName, cacheCtx);
|
|
@@ -5892,11 +5981,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
|
|
|
5892
5981
|
const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
|
|
5893
5982
|
const executor = resolveExecutor(db, context);
|
|
5894
5983
|
const uniqueIds = [...new Set(parentIds)];
|
|
5895
|
-
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(
|
|
5896
|
-
const
|
|
5897
|
-
[GROUP_KEY]:
|
|
5984
|
+
const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, (0, drizzle_orm.and)((0, drizzle_orm.inArray)(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
|
|
5985
|
+
const selected = executor.select({
|
|
5986
|
+
[GROUP_KEY]: groupCol,
|
|
5898
5987
|
...request.selection
|
|
5899
|
-
}).from(targetTable)
|
|
5988
|
+
}).from(targetTable);
|
|
5989
|
+
const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(groupCol);
|
|
5900
5990
|
const byKey = new Map(rows.map((row) => [row[GROUP_KEY], row]));
|
|
5901
5991
|
return parentIds.map((id) => byKey.get(id) ?? {});
|
|
5902
5992
|
});
|