@querykitjs/prisma-pg 1.0.1 → 2.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@querykitjs/prisma-pg",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "Advanced filtering + flexible pagination repository layer for Prisma (PostgreSQL).",
5
5
  "type": "module",
6
6
  "author": {
@@ -23,13 +23,19 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "exports": {
25
25
  ".": {
26
- "types": "./dist/index.d.ts",
27
- "import": "./dist/index.js",
28
- "require": "./dist/index.cjs"
26
+ "import": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.cts",
32
+ "default": "./dist/index.cjs"
33
+ }
29
34
  }
30
35
  },
31
36
  "files": [
32
37
  "dist",
38
+ "skills",
33
39
  "README.md",
34
40
  "README.en.md",
35
41
  "LICENSE"
@@ -38,6 +44,9 @@
38
44
  "publishConfig": {
39
45
  "access": "public"
40
46
  },
47
+ "engines": {
48
+ "node": ">=22.12"
49
+ },
41
50
  "scripts": {
42
51
  "build": "tsup",
43
52
  "dev": "tsup --watch",
@@ -45,18 +54,21 @@
45
54
  "test:generate": "prisma generate --schema test/prisma/schema.prisma",
46
55
  "test:query": "bun run test/query.ts",
47
56
  "test:types": "tsc --noEmit -p test/tsconfig.json",
48
- "test:contract": "bun --preload ../mongoose/test/_bun-v8-shim.ts test/contract.ts",
57
+ "test:contract": "bun --preload ../mongoose/test/bun-v8-shim.ts test/contract.ts",
49
58
  "test:smoke": "bun run test/smoke.ts"
50
59
  },
51
60
  "dependencies": {
52
- "@querykitjs/core": "^1.2.0"
61
+ "@querykitjs/core": "^2.1.0"
53
62
  },
54
63
  "peerDependencies": {
55
- "@prisma/client": ">=5"
64
+ "@prisma/client": "^6.19 || ^7.10"
56
65
  },
57
66
  "devDependencies": {
58
- "@prisma/client": "^6.19.1",
59
- "prisma": "^6.19.1"
67
+ "@prisma/adapter-pg": "7.10.1",
68
+ "@prisma/client": "7.10.1",
69
+ "@types/pg": "8.23.1",
70
+ "pg": "8.23.1",
71
+ "prisma": "7.10.1"
60
72
  },
61
73
  "keywords": [
62
74
  "prisma",
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: querykit-prisma-pg
3
+ description: Use when code builds or calls a querykit repository over Prisma 6.19 / 7 and PostgreSQL (@querykitjs/prisma-pg) - createRegistry(prisma), registry.repository(prisma.model, options), filterable / sortable / limits policy, fields overrides, forcedColumns / allowedColumns, relations and with, findList / findInfinite / findCursor, updateWhere / deleteWhere, scoped, softDelete, upsert, aggregate, or registry.transaction.
4
+ ---
5
+
6
+ ## Safety rules
7
+
8
+ - DO list what a client may filter on in `filterable` (field → operators; a relation field is `"posts.title"`). `"*"` admits every operator, so DON'T put `"*"` on a secret field. Without `filterable` a client gets only `=` / `in` on `allowedColumns` (else the `forcedColumns` keys, else only the `@id` field). Anything else is `QueryKitError` `forbidden-key` / `forbidden-operator` before Prisma is called.
9
+ - DO list sortable fields in `sortable`. The default is the `@id` field and `createdAt`, and a client `cursorKey` must be in it too.
10
+ - DO declare `forcedColumns` (always these fields) or `allowedColumns` (the client may narrow them) on the repository. They guard reads only.
11
+ - DON'T pass a plain Prisma `where` object from request data as a filter node. Server code brands one with `raw()` from `@querykitjs/core`; an unbranded object is `forbidden-key`. `raw()` skips `filterable`.
12
+ - DO list every relation a client may load in `relations`, with its `columns` (a list, or `"*"` on purpose). An unlisted relation, column, filter field or nested relation is `forbidden-key`, `site: "with"`.
13
+ - DO keep the request limits. They are core `DEFAULT_QUERY_LIMITS`: depth `5`, conditions `30`, array items `200`, string length `256`, offset `10000`, sort fields `3`, LIKE wildcards `2`. Page size is capped at `200` (`maxPerPage` / `maxLimit`). Raise one with `limits` on the registry or the repository. DON'T hard-code your own checks.
14
+ - DO set `strict: true` (a dropped condition throws) or `onSkippedCondition` (a dropped condition is reported). By default an unknown key is skipped silently.
15
+ - DON'T call `updateWhere` / `deleteWhere` without a filter to mean "all rows": that is `empty-filter`. Pass `{ all: true }` on purpose. The scope still applies.
16
+ - DO set `cursorSecret` on the registry when cursor tokens leave the server. Unsigned or altered tokens are then `invalid-value`.
17
+ - DO declare in `fields` what the Prisma runtime model does not say: `nativeType: "Uuid"` (else a malformed uuid reaches PostgreSQL as a driver error) and `isRequired: false` for optional fields on Prisma 7 (else NULL order stays PostgreSQL's).
18
+ - DON'T call HTTP, queues or email inside `registry.transaction`: the callback may run again, and only database work rolls back.
19
+
20
+ ## When to use
21
+
22
+ - A Node backend on Prisma 6.19 or 7 with PostgreSQL (`prisma-client` generator + `@prisma/adapter-pg`) that serves list requests from `@querykitjs/web`.
23
+ - Offset, infinite and cursor pages, RBAC / tenant scoping, soft delete, bulk writes, upserts and aggregation over one model.
24
+ - Typed results: `columns` and `with` infer the row type.
25
+
26
+ ## Setup
27
+
28
+ ```ts
29
+ import { createRegistry } from "@querykitjs/prisma-pg";
30
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
31
+
32
+ declare const prisma: PrismaClient; // new PrismaClient({ adapter: new PrismaPg({ connectionString }) })
33
+
34
+ export const registry = createRegistry(prisma, { strict: true, cursorSecret: process.env.CURSOR_SECRET, limits: { maxInItems: 100 } });
35
+
36
+ export const usersRepo = registry.repository(prisma.user, {
37
+ forcedColumns: { id: true, name: true, age: true, createdAt: true },
38
+ filterable: { name: ["contains", "="], age: [">=", "<=", "between"], createdAt: ["between"] },
39
+ sortable: ["id", "name", "createdAt"],
40
+ fields: [{ name: "age", isRequired: false }],
41
+ });
42
+ ```
43
+
44
+ ## Common tasks
45
+
46
+ ### Filter with the helper and a raw `where`
47
+
48
+ ```ts
49
+ import { raw } from "@querykitjs/core";
50
+ import { createFilters, createRegistry } from "@querykitjs/prisma-pg";
51
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
52
+
53
+ declare const prisma: PrismaClient;
54
+ const f = createFilters<typeof prisma.user>(); // field-name autocomplete, raw() nodes allowed
55
+ const usersRepo = createRegistry(prisma).repository(prisma.user, { filterable: { age: [">="], name: ["contains"] } });
56
+
57
+ const adults = await usersRepo.findAll({
58
+ filter: f.and(f.gte("age", 18), f.or(f.contains("name", "ali"), raw({ posts: { some: { title: "querykit" } } }))),
59
+ });
60
+ ```
61
+
62
+ ### Offset page with a multi-field sort
63
+
64
+ ```ts
65
+ import { createRegistry } from "@querykitjs/prisma-pg";
66
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
67
+
68
+ declare const prisma: PrismaClient;
69
+ const usersRepo = createRegistry(prisma).repository(prisma.user, { sortable: ["id", "name", "createdAt"] });
70
+
71
+ const page = await usersRepo.findList({
72
+ page: 2,
73
+ perPage: 20,
74
+ sort: [
75
+ { key: "name", direction: "asc" },
76
+ { key: "createdAt", direction: "desc" },
77
+ ],
78
+ withTotal: false, // no count query: total_items / total_pages are null, has_next still works
79
+ });
80
+ console.log(page.meta.has_next);
81
+ ```
82
+
83
+ ### Infinite scroll
84
+
85
+ ```ts
86
+ import { createRegistry } from "@querykitjs/prisma-pg";
87
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
88
+
89
+ declare const prisma: PrismaClient;
90
+ const usersRepo = createRegistry(prisma).repository(prisma.user);
91
+
92
+ const feed = await usersRepo.findInfinite({ limit: 20, offset: 40 });
93
+ console.log(feed.meta.has_more, feed.meta.next_offset);
94
+ ```
95
+
96
+ ### Cursor (keyset) pages
97
+
98
+ ```ts
99
+ import { createRegistry } from "@querykitjs/prisma-pg";
100
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
101
+
102
+ declare const prisma: PrismaClient;
103
+ const usersRepo = createRegistry(prisma).repository(prisma.user);
104
+
105
+ const first = await usersRepo.findCursor({ limit: 20, cursorKey: "createdAt", order: "desc" });
106
+ const next = await usersRepo.findCursor({ limit: 20, cursorKey: "createdAt", order: "desc", cursor: first.meta.next_cursor });
107
+ const back = await usersRepo.findCursor({ limit: 20, cursorKey: "createdAt", order: "desc", cursor: next.meta.prev_cursor, direction: "backward" });
108
+ console.log(back.data.length);
109
+ ```
110
+
111
+ ### Relations with `with`
112
+
113
+ ```ts
114
+ import { createRegistry } from "@querykitjs/prisma-pg";
115
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
116
+
117
+ declare const prisma: PrismaClient;
118
+ const usersRepo = createRegistry(prisma).repository(prisma.user, {
119
+ relations: { posts: { columns: ["id", "title"] } },
120
+ });
121
+
122
+ const rows = await usersRepo.findAll({ columns: { id: true, name: true }, with: { posts: { take: 5 } } });
123
+ console.log(rows[0]?.posts[0]?.title);
124
+ ```
125
+
126
+ ### Scope every call to some owners
127
+
128
+ ```ts
129
+ import { createRegistry } from "@querykitjs/prisma-pg";
130
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
131
+
132
+ declare const prisma: PrismaClient;
133
+ declare const authorIds: number[];
134
+ const postsRepo = createRegistry(prisma).repository(prisma.post);
135
+
136
+ const mine = postsRepo.scoped({ authorId: { in: authorIds } }); // { in: [] } matches no row
137
+ await mine.findList({ page: 1 });
138
+ ```
139
+
140
+ ### Bulk update / delete, upsert
141
+
142
+ ```ts
143
+ import { createRegistry } from "@querykitjs/prisma-pg";
144
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
145
+
146
+ declare const prisma: PrismaClient;
147
+ const usersRepo = createRegistry(prisma).repository(prisma.user, { filterable: { age: ["<", "isNull"] } });
148
+
149
+ const { count } = await usersRepo.updateWhere([{ key: "age", operation: "<", value: 18 }], { age: 18 });
150
+ const { rows } = await usersRepo.deleteWhere([{ key: "age", operation: "isNull" }], { returning: { limit: 50 } });
151
+ await usersRepo.upsert({ name: "Ali", email: "ali@example.com" }, { target: "email" });
152
+ console.log(count, rows.length);
153
+ ```
154
+
155
+ ### Soft delete, aggregate and transaction
156
+
157
+ ```ts
158
+ import { createRegistry } from "@querykitjs/prisma-pg";
159
+ import type { PrismaClient } from "@querykitjs/prisma-pg/skill-prelude";
160
+
161
+ declare const prisma: PrismaClient;
162
+ const registry = createRegistry(prisma);
163
+ const postsRepo = registry.repository(prisma.post, { filterable: { authorId: ["="] } }); // a deletedAt field turns soft delete on
164
+
165
+ await postsRepo.softDelete(1);
166
+ await postsRepo.restore(1);
167
+ const perAuthor = await postsRepo.aggregate({ count: true, groupBy: "authorId" }); // groupBy / sum / … fields must be in filterable
168
+ await registry.transaction(async () => {
169
+ await postsRepo.create({ title: "Hello", authorId: 1 });
170
+ await postsRepo.updateById(1, { title: "Hello again" });
171
+ }); // a throw rolls both back
172
+ console.log(perAuthor);
173
+ ```
174
+
175
+ ## Request contract
176
+
177
+ The same JSON shape as every querykit package (`@querykitjs/core`): conditions `{ key, operation?, value? }`, groups `{ and }` / `{ or }` / `{ not }` (a flat array = AND), `sort` always `[{ key, direction? }]`, offset `page` / `perPage` / `withTotal`, infinite `limit` / `offset`, cursor `limit` / `cursor` / `cursorKey` / `order` / `direction` (no `sort`). Meta is snake_case: `total_items`, `total_pages`, `has_next`, `has_more`, `next_offset`, `next_cursor`, `prev_cursor`. The body types are `WireOffsetParams`, `WireInfiniteParams` and `WireCursorParams`; validate with `@querykitjs/zod` or `@querykitjs/class-validator`, then pass the body as is. Refusals are `QueryKitError` with `{ source, site, key, operation?, reason }`.
178
+
179
+ ## Mistakes to avoid
180
+
181
+ - Relying on an index on `(col)` alone for `findList` or `findInfinite`: they end the order on the primary key, so a large table needs a compound index on `(col, primary key)`. The key does not count against `maxSortFields`; `findAll` and `findOne` do not add it.
182
+ - An unescaped `_` in `like` / `ilike`: Prisma has no one-character wildcard, so it is `invalid-value`. Write `\\_` for a literal underscore.
183
+ - Using the database column name (`created_at`) as a key: keys are Prisma field names (`createdAt`).
184
+ - Using a text operator on a non-`String` field: it is skipped as `invalid-value`.
185
+ - Calling `updateById` / `deleteById` / `softDelete` / `restore` on a composite `@@id`: use `updateWhere` / `deleteWhere`.
186
+ - Expecting `?schema=` in the connection string to work with the driver adapter: pass `{ schema }` to `PrismaPg` instead.