@querykitjs/prisma-pg 1.0.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/LICENSE +21 -0
- package/README.en.md +363 -0
- package/README.md +362 -0
- package/dist/index.cjs +844 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +494 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suhrobbek Soatov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<div align="right">
|
|
2
|
+
|
|
3
|
+
**English** · [O'zbekcha](./README.md)
|
|
4
|
+
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
# @querykitjs/prisma-pg
|
|
8
|
+
|
|
9
|
+
> A repository layer over **Prisma (PostgreSQL)** with advanced filtering and flexible pagination.
|
|
10
|
+
|
|
11
|
+
Everything you would otherwise hand-write on top of Prisma — nested filters, three pagination styles, transactions, RBAC scoping, soft-delete, bulk upserts and aggregation — behind one typed repository. Result types are inferred **automatically** from `columns`/`with`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const { data, meta } = await usersRepository.findList({
|
|
15
|
+
page: 2,
|
|
16
|
+
perPage: 20,
|
|
17
|
+
filter: f.and(f.eq("status", "active"), f.gte("age", 18)),
|
|
18
|
+
sort: [{ key: "createdAt", direction: "desc" }],
|
|
19
|
+
});
|
|
20
|
+
// ^? data: User[] meta: { total_items, total_pages, has_next, ... }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Advanced filters** — nested `and`/`or`/`not`, 27 operators, raw Prisma `where` escape hatch, `f` helpers.
|
|
26
|
+
- **Three pagination styles** — offset (`findList`), infinite scroll (`findInfinite`), cursor/keyset (`findCursor`).
|
|
27
|
+
- **`columns` + `with` inference** — selected fields and loaded relations are typed with no manual generics.
|
|
28
|
+
- **Multi-field sort**, `count`, `exists`, `aggregate` (count/sum/avg/min/max + groupBy).
|
|
29
|
+
- **Transactions** — `registry.transaction()`; repositories inside it join automatically.
|
|
30
|
+
- **Scoped repositories** — a constant base filter + insert defaults, for RBAC / multi-tenancy.
|
|
31
|
+
- **Soft delete** — automatic when the model has `deletedAt`; `softDelete`/`restore`/`withDeleted`.
|
|
32
|
+
- **Upsert / bulk** — `upsert`, `upsertMany`.
|
|
33
|
+
- **Framework-agnostic** — it knows nothing about HTTP: Express, Hono, NestJS, background jobs, all the same.
|
|
34
|
+
- **Cross-adapter parity** — the same surface and the same results as `@querykitjs/drizzle-pg` and `@querykitjs/mongoose` ([PARITY.md](./PARITY.md)).
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun add @querykitjs/prisma-pg @prisma/client
|
|
40
|
+
# or: npm i @querykitjs/prisma-pg @prisma/client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`@prisma/client` (>=5) is a peer dependency. The package never **imports** your generated client: it reads model metadata off your client instance at runtime and derives types from the delegate you pass in.
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
### 1. Registry (once)
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// db/registry.ts
|
|
51
|
+
import { PrismaClient } from "@prisma/client";
|
|
52
|
+
import { createRegistry } from "@querykitjs/prisma-pg";
|
|
53
|
+
|
|
54
|
+
export const prisma = new PrismaClient();
|
|
55
|
+
export const registry = createRegistry(prisma);
|
|
56
|
+
// or with pagination defaults:
|
|
57
|
+
// export const registry = createRegistry(prisma, { defaultPerPage: 20, defaultLimit: 20 });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Repository (per model)
|
|
61
|
+
|
|
62
|
+
A model is taken by its **handle**, the way drizzle-pg takes a table object and mongoose takes a `Model`. Its name (the delegate key, camelCase) works too: `User` → `"user"`, `LegalEntity` → `"legalEntity"`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// db/users.repository.ts
|
|
66
|
+
import { prisma, registry } from "./registry";
|
|
67
|
+
|
|
68
|
+
export const usersRepository = registry.repository(prisma.user);
|
|
69
|
+
// …or by name: registry.repository("user") — the two are equivalent
|
|
70
|
+
|
|
71
|
+
// with custom methods:
|
|
72
|
+
export const postsRepository = registry.repository("post", base => ({
|
|
73
|
+
findBySlug: (slug: string) => base.findOne({ filter: [{ key: "slug", value: slug }] }),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// with repository options (RBAC / projection):
|
|
77
|
+
export const safeUsers = registry.repository("user", {
|
|
78
|
+
forcedColumns: { id: true, name: true, email: true }, // a password can never leak
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Framework-agnostic: Express, Hono, NestJS
|
|
83
|
+
|
|
84
|
+
This package knows nothing about HTTP. Parse and validate the request in your app (`@querykitjs/zod` or `@querykitjs/class-validator`); the repository call is identical everywhere.
|
|
85
|
+
|
|
86
|
+
**Express**
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import express from "express";
|
|
90
|
+
import { offsetParamsSchema } from "@querykitjs/zod";
|
|
91
|
+
import { usersRepository } from "./db/users.repository";
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
app.post("/users/list", express.json(), async (req, res) => {
|
|
95
|
+
const params = offsetParamsSchema.parse(req.body);
|
|
96
|
+
res.json(await usersRepository.findList(params));
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Hono**
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Hono } from "hono";
|
|
104
|
+
import { offsetParamsSchema } from "@querykitjs/zod";
|
|
105
|
+
import { usersRepository } from "./db/users.repository";
|
|
106
|
+
|
|
107
|
+
const app = new Hono();
|
|
108
|
+
app.post("/users/list", async c => {
|
|
109
|
+
const params = offsetParamsSchema.parse(await c.req.json());
|
|
110
|
+
return c.json(await usersRepository.findList(params));
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**NestJS**
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
118
|
+
import { OffsetParamsDto } from "@querykitjs/class-validator";
|
|
119
|
+
import { usersRepository } from "./db/users.repository";
|
|
120
|
+
|
|
121
|
+
@Controller("users")
|
|
122
|
+
export class UsersController {
|
|
123
|
+
@Post("list")
|
|
124
|
+
list(@Body() params: OffsetParamsDto) {
|
|
125
|
+
return usersRepository.findList(params);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A validated payload goes into the repository params with **no cast** — the contract is closed through `@querykitjs/core`.
|
|
131
|
+
|
|
132
|
+
## Advanced filters
|
|
133
|
+
|
|
134
|
+
A filter is a tree of field conditions and logical groups (`and`/`or`/`not`). A flat array is implicit `AND`.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { createFilters } from "@querykitjs/prisma-pg";
|
|
138
|
+
const f = createFilters<typeof prisma.user>(); // field-name autocomplete
|
|
139
|
+
|
|
140
|
+
await usersRepository.findAll({
|
|
141
|
+
filter: f.and(f.eq("status", "active"), f.or(f.gte("age", 18), f.in("role", ["admin", "owner"])), f.not(f.isNull("deletedAt"))),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// object form (no helpers):
|
|
145
|
+
await usersRepository.findAll({
|
|
146
|
+
filter: [
|
|
147
|
+
{ key: "status", operation: "=", value: "active" },
|
|
148
|
+
{ key: "name", operation: "%_%", value: "ali" },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Operators
|
|
154
|
+
|
|
155
|
+
| Group | Operators |
|
|
156
|
+
| ---------- | -------------------------------------------------------------------------------- |
|
|
157
|
+
| Comparison | `=`/`eq`, `!=`/`ne`, `>`/`gt`, `>=`/`gte`, `<`/`lt`, `<=`/`lte` |
|
|
158
|
+
| Text | `contains`/`%_%`, `startsWith`/`%_`, `endsWith`/`_%`, `like`, `ilike`, `notLike` |
|
|
159
|
+
| Set | `in`, `notIn`, `between`, `notBetween` |
|
|
160
|
+
| NULL | `isNull`, `isNotNull` |
|
|
161
|
+
|
|
162
|
+
`contains`/`startsWith`/`endsWith` are case-**insensitive** (`mode: "insensitive"`), `like` is case-**sensitive**, `ilike` is insensitive — exactly as in the drizzle-pg adapter.
|
|
163
|
+
|
|
164
|
+
> **LIKE patterns.** Prisma has no raw SQL `LIKE` inside `where`, so the pattern is translated — **exactly**, so any pattern returns the same rows as on drizzle-pg: `%ali%` → `contains`, `ali%` → `startsWith`, `%ali` → `endsWith`, no wildcard → `equals`. Interior `%` and `_` are handled with two identities — `LIKE 'A%S'` ≡ `LIKE 'A%S%' AND LIKE '%S'`, and for a pattern with no `%`, `LIKE 'P'` ≡ `LIKE 'P%' AND NOT LIKE 'P_%'` (which pins the length). Only a **malformed** pattern (a dangling trailing `\`) is dropped and reported. Details: [PARITY.md](./PARITY.md) §5.1.
|
|
165
|
+
|
|
166
|
+
### Raw `where` escape hatch
|
|
167
|
+
|
|
168
|
+
For anything the DSL cannot express (relation predicates, JSON filters), pass a Prisma `where` object as a filter node:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
await usersRepository.findAll({ filter: { posts: { some: { title: { contains: "querykit" } } } } });
|
|
172
|
+
|
|
173
|
+
await usersRepository.findAll({
|
|
174
|
+
filter: f.or(f.eq("role", "admin"), { posts: { some: { published: true } } }),
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Unknown keys: watch, then enforce
|
|
179
|
+
|
|
180
|
+
An unknown filter/sort key is **dropped silently** (legacy parity). That widens results, so you can make it visible:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// step 1: watch (behaviour unchanged)
|
|
184
|
+
createRegistry(prisma, { onSkippedCondition: info => logger.warn({ querykit: info }, "condition dropped") });
|
|
185
|
+
|
|
186
|
+
// step 2: once the log is clean — enforce
|
|
187
|
+
createRegistry(prisma, { strict: true });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { QueryKitError } from "@querykitjs/core";
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await usersRepository.findList(params);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof QueryKitError) return res.status(400).json({ error: err.message, info: err.info });
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A `scope` or `cursorKey` key is **always** fatal regardless of `strict` — those are the server's own, and dropping them silently would be a security hole.
|
|
202
|
+
|
|
203
|
+
## Multi-field sort
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
await usersRepository.findAll({
|
|
207
|
+
sort: [
|
|
208
|
+
{ key: "role", direction: "asc" },
|
|
209
|
+
{ key: "createdAt", direction: "desc" },
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Unknown keys are skipped; if no valid sort remains it falls back to `createdAt desc`, then `<id> desc`, so pagination stays stable.
|
|
215
|
+
|
|
216
|
+
## Pagination — three strategies
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// 1) Offset — with total_items/total_pages
|
|
220
|
+
const list = await usersRepository.findList({ page: 2, perPage: 20 });
|
|
221
|
+
list.meta; // { total_items, total_pages, current_page, per_page, has_next, has_prev }
|
|
222
|
+
|
|
223
|
+
// 2) Infinite scroll — no COUNT
|
|
224
|
+
const feed = await postsRepository.findInfinite({ limit: 20, offset: 40 });
|
|
225
|
+
feed.meta; // { limit, offset, count, has_more, next_offset }
|
|
226
|
+
|
|
227
|
+
// 3) Cursor (keyset) — stable under inserts
|
|
228
|
+
const p1 = await postsRepository.findCursor({ limit: 20, order: "desc" });
|
|
229
|
+
const p2 = await postsRepository.findCursor({ limit: 20, cursor: p1.meta.next_cursor });
|
|
230
|
+
p1.meta; // { limit, has_next, has_prev, next_cursor, prev_cursor }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`perPage`/`limit` are **capped** (`DEFAULT_MAX_PER_PAGE` = 200) — the repository clamps even if validation was bypassed.
|
|
234
|
+
|
|
235
|
+
> Cursor tokens are **byte-identical** to the drizzle-pg and mongoose adapters', so an old token keeps working if the backend swaps ORMs.
|
|
236
|
+
|
|
237
|
+
## Typed `columns` and `with`
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const picked = await usersRepository.findAll({ columns: { id: true, email: true } });
|
|
241
|
+
// ^? { id: number; email: string }[] — `name` is not even in the type
|
|
242
|
+
|
|
243
|
+
const withPosts = await usersRepository.findAll({ with: { posts: true } });
|
|
244
|
+
withPosts[0].posts[0].title; // the relation is typed
|
|
245
|
+
|
|
246
|
+
const both = await usersRepository.findAll({ columns: { id: true }, with: { posts: true } });
|
|
247
|
+
// ^? { id: number; posts: Post[] }[]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
> ⚠️ Prisma rejects `select` and `include` **at the same level**. The adapter handles it: when both are given, relations are placed inside a single `select`. You never see the difference.
|
|
251
|
+
|
|
252
|
+
A `with` value may be `true` or a Prisma relation config:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await usersRepository.findAll({ with: { posts: { select: { title: true }, take: 5, orderBy: { createdAt: "desc" } } } });
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Transactions
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
await registry.transaction(async () => {
|
|
262
|
+
const user = await usersRepository.create({ name: "Ali", email: "ali@example.com" });
|
|
263
|
+
await postsRepository.create({ title: "Hello", authorId: user.id });
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Repositories inside pick up the transaction client through ambient context (AsyncLocalStorage) — no `tx` threading. Any throw rolls everything back.
|
|
268
|
+
|
|
269
|
+
> ⚠️ Prisma has no savepoints, so a **nested** `transaction()` reuses the current one instead of opening another. The mongoose adapter behaves the same; drizzle-pg creates a real savepoint.
|
|
270
|
+
|
|
271
|
+
## Scoped repositories (RBAC / multi-tenancy)
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
const mine = postsRepository.scoped({ authorId: user.id });
|
|
275
|
+
await mine.findList({ page: 1 }); // only this user's posts
|
|
276
|
+
await mine.create({ title: "New" }); // authorId filled in automatically
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The scope is `AND`ed into every read/write and merged into inserts (scope wins). If a scope key does not resolve on the model, building the repository **throws** — dropping it silently would remove the filter.
|
|
280
|
+
|
|
281
|
+
## Projection guards
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
registry.repository("user", { forcedColumns: { id: true, name: true } }); // client `columns` ignored
|
|
285
|
+
registry.repository("user", { allowedColumns: ["id", "name", "email"] }); // client selection intersected
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
An empty intersection yields the allowlist, **never** the full row. The guard also covers `aggregate`, and reading a forbidden field through `cursorKey` is prevented.
|
|
289
|
+
|
|
290
|
+
> ⚠️ Guards affect **read** methods only. Write methods return the full `Row` by type contract — re-read with `findById` before handing a write result to a client.
|
|
291
|
+
|
|
292
|
+
## Soft delete
|
|
293
|
+
|
|
294
|
+
Enabled automatically when the model has a `deletedAt` field:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
await postsRepository.softDelete(id); // deletedAt = now()
|
|
298
|
+
await postsRepository.findAll({}); // deleted rows are excluded
|
|
299
|
+
await postsRepository.findAll({ withDeleted: true }); // everything
|
|
300
|
+
await postsRepository.restore(id); // deletedAt = null
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
An `updatedAt` field is bumped on every update (unless it is `@updatedAt`, which Prisma maintains itself — the adapter then stays out of the way).
|
|
304
|
+
|
|
305
|
+
## Upsert, bulk and aggregation
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
await usersRepository.upsert({ email: "a@b.com", name: "Ali" }, { target: "email" });
|
|
309
|
+
await usersRepository.upsertMany(rows, { target: "externalId" });
|
|
310
|
+
|
|
311
|
+
await postsRepository.aggregate({ count: true, groupBy: "authorId" });
|
|
312
|
+
// → [{ authorId: 1, count: 12 }, …]
|
|
313
|
+
await ordersRepository.aggregate({ sum: "amount", avg: "amount", groupBy: "region" });
|
|
314
|
+
// → [{ region: "TAS", sum_amount: 900, avg_amount: 75 }, …]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
> **Compound unique.** A multi-field `target` uses Prisma's generated name: `{ target: ["a", "b"] }` → `where: { a_b: { a, b } }`. For a constraint declared with `@@unique(name: "...")`, use the raw client.
|
|
318
|
+
>
|
|
319
|
+
> **Bulk.** Prisma has no bulk upsert, so `upsertMany` issues sequential `upsert`s (input order preserved). Wrap large imports in `registry.transaction()`.
|
|
320
|
+
|
|
321
|
+
## Limitations
|
|
322
|
+
|
|
323
|
+
- `updateById` / `deleteById` / `softDelete` / `restore` require a **single-field `@id`**. Prisma's `update`/`delete` accept only a unique `where`, while querykit's predicate also carries the scope and soft-delete guards. With a composite `@@id`, use `updateWhere`/`deleteWhere`.
|
|
324
|
+
- Filter/sort keys are **Prisma field names** (`createdAt`), not DB columns (`created_at`) — Prisma's own `where` does not accept a `@map`ped name either.
|
|
325
|
+
- Text operators only apply to `String` fields; on any other type the condition is dropped and reported.
|
|
326
|
+
|
|
327
|
+
Full list and the three-adapter comparison: [PARITY.md](./PARITY.md).
|
|
328
|
+
|
|
329
|
+
## API reference
|
|
330
|
+
|
|
331
|
+
| Method | Description |
|
|
332
|
+
| -------------------------------------------- | -------------------------------------- |
|
|
333
|
+
| `findAll(params?)` | All matching rows |
|
|
334
|
+
| `findOne(params?)` / `findById(id, params?)` | One row or `undefined` |
|
|
335
|
+
| `findList(params?)` | Offset pagination — `{ data, meta }` |
|
|
336
|
+
| `findInfinite(params?)` | Infinite scroll — `{ data, meta }` |
|
|
337
|
+
| `findCursor(params?)` | Cursor/keyset — `{ data, meta }` |
|
|
338
|
+
| `count(filter?)` / `exists(filter?)` | Count / existence |
|
|
339
|
+
| `create` / `createMany` | Insert |
|
|
340
|
+
| `upsert` / `upsertMany` | Insert or update on conflict |
|
|
341
|
+
| `updateById` / `updateWhere` | Update (+ `updatedAt` bump) |
|
|
342
|
+
| `deleteById` / `deleteWhere` | Hard delete |
|
|
343
|
+
| `softDelete` / `restore` | Soft delete / restore |
|
|
344
|
+
| `aggregate(spec)` | count/sum/avg/min/max + groupBy |
|
|
345
|
+
| `scoped(scope)` | A new repository with a constant scope |
|
|
346
|
+
|
|
347
|
+
`createRegistry(prisma, options?)` — `defaultPerPage`, `defaultLimit`, `maxPerPage`, `maxLimit`, `strict`, `onSkippedCondition`.
|
|
348
|
+
|
|
349
|
+
## Development
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
bun run build
|
|
353
|
+
bun run typecheck
|
|
354
|
+
bun run test:generate # fixture Prisma client (no database needed)
|
|
355
|
+
bun run test:query # compiler tests (no database)
|
|
356
|
+
bun run test:contract # cross-adapter + web contract audit (no database)
|
|
357
|
+
bun run test:types # type-inference test
|
|
358
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/db bun run test:smoke
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## License
|
|
362
|
+
|
|
363
|
+
MIT
|