query-builder-prisma 0.0.0-stage → 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mamun
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.md CHANGED
@@ -1,3 +1,255 @@
1
- # Temporary Holding Version
1
+ # query-builder-prisma
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/query-builder-prisma.svg)](https://www.npmjs.com/package/query-builder-prisma)
4
+ [![CI](https://github.com/almamun2b/query-builder-prisma/actions/workflows/ci.yml/badge.svg)](https://github.com/almamun2b/query-builder-prisma/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/query-builder-prisma.svg)](./LICENSE)
6
+ [![node](https://img.shields.io/node/v/query-builder-prisma.svg)](https://www.npmjs.com/package/query-builder-prisma)
7
+
8
+ > Zero-coupling, fluent, fully type-safe query builder for any Prisma model
9
+ > delegate. Works with **every Prisma version — 5, 6, 7, 8 and future majors by
10
+ > design** — and every database Prisma supports.
11
+
12
+ ```ts
13
+ const { meta, data } = await new QueryBuilder(prisma.user)
14
+ .search({ searchText: q, fields: ['email', 'username'] }) // dialect-aware
15
+ .filter({ role, status }) // undefined values stripped
16
+ .range({ createdAt: { from, to }, 'avatar.size': { from: min } })
17
+ .sortBy({ sortBy: 'createdAt', sortOrder: 'desc' })
18
+ .paginate({ page: 1, limit: 20 }) // or .cursorPaginate({...})
19
+ .select({ id: true, email: true }) // output type narrows structurally
20
+ .executeWithMeta();
21
+ // meta: { page: 1, limit: 20, total: 142, totalPage: 8 }
22
+ ```
23
+
24
+ ## Why
25
+
26
+ An npm package that imports Prisma types breaks on Prisma's next major — import
27
+ paths changed in v7, the type utilities move in v8, the engine was rewritten.
28
+ This package speaks only Prisma's stable public contract, the **model-delegate
29
+ call shape**:
30
+
31
+ ```
32
+ model.findMany({ where, orderBy, skip, take, select, include, cursor, distinct })
33
+ model.count({ where })
34
+ ```
35
+
36
+ It imports **nothing** from Prisma — no runtime imports, no type imports, no
37
+ peer dependencies. All types are derived structurally from the delegate you
38
+ pass in, so a Prisma upgrade can never break it. The test suite runs with zero
39
+ Prisma installed; that is the standing proof.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ npm install query-builder-prisma
45
+ ```
46
+
47
+ Published on npm as [`query-builder-prisma`](https://www.npmjs.com/package/query-builder-prisma).
48
+
49
+ Node ≥ 18; TypeScript ≥ 5 for inference (plain JS works). Dual ESM + CJS; zero
50
+ runtime dependencies.
51
+
52
+ ## Documentation
53
+
54
+ | | |
55
+ | -------------------------------------------------------------------- | -------------------------------------------------- |
56
+ | [Getting started](./docs/getting-started.md) | install, first query, imports |
57
+ | [Guides](./docs/index.md#guides) | filtering, sorting, both pagination modes, projections, dialects, advanced patterns |
58
+ | [API reference](./docs/api/query-builder.md) | every method, type and function |
59
+ | [Use cases](./docs/examples/use-cases.md) | end-to-end scenarios |
60
+ | [Framework recipes](./docs/framework-recipes.md) | Next.js, NestJS, Express, edge runtimes |
61
+
62
+ ## Features
63
+
64
+ - **Fluent chain** — `search`, `filter`, `range`, `where`, `sortBy`/`orderBy`,
65
+ `paginate`/`cursorPaginate`, `select`/`include`, `custom`, folded into one
66
+ `findMany` call.
67
+ - **Offset pagination with metadata** — `executeWithMeta()` returns
68
+ `page`/`limit`/`total`/`totalPage`, with `COUNT` + `findMany` in parallel and
69
+ an opt-in fast path (`includeTotal: false`) that skips the `COUNT`.
70
+ - **Cursor (seek) pagination** — stable deep pages for feeds and infinite
71
+ scroll, forward/backward walking, ISO-8601-safe `Date` cursors, exact flags
72
+ via `strict: true`.
73
+ - **Structural type projection** — `.select()`/`.include()` narrow result types
74
+ like Prisma's own overloads, nested includes included, without Prisma types.
75
+ - **Dialect capability registry** — `mode: 'insensitive'` only where Prisma
76
+ accepts it; new databases are one `registerDialect()` call.
77
+ - **Hardened by default** — `undefined` stripping at any depth, prototype-key
78
+ refusal, clamped (or thrown) pagination numbers, a seed `where` that cannot
79
+ be replaced.
80
+
81
+ ### Offsetting and counting
82
+
83
+ ```ts
84
+ const { meta, data } = await new QueryBuilder(prisma.user)
85
+ .filter({ status: 'ACTIVE' })
86
+ .paginate({ page: 2, limit: 20 })
87
+ .executeWithMeta(); // meta: { page: 2, limit: 20, total, totalPage }
88
+
89
+ // Skip the COUNT query on large tables:
90
+ await qb.executeWithMeta({ includeTotal: false }); // total: null, totalPage: null
91
+ ```
92
+
93
+ ### Cursor pagination
94
+
95
+ ```ts
96
+ const page = await new QueryBuilder(prisma.post)
97
+ .sortBy({ sortBy: 'createdAt', sortOrder: 'desc' })
98
+ .cursorPaginate({ cursor: prev?.nextCursor, cursorField: 'createdAt', limit: 10 })
99
+ .executeWithCursor();
100
+ // meta: { limit, nextCursor, prevCursor, hasNextPage, hasPrevPage }
101
+ // rows always in configured sort order, whatever the walking direction
102
+ ```
103
+
104
+ ### Type-safe projections
105
+
106
+ ```ts
107
+ const users = await new QueryBuilder(prisma.user)
108
+ .select({ id: true, posts: { select: { title: true } } })
109
+ .execute();
110
+ // users: { id: string; posts: { title: string }[] }[] — no Prisma types involved
111
+
112
+ // 100% Prisma-native inference whenever you want it:
113
+ await prisma.user.findMany(qb.buildArgs());
114
+ ```
115
+
116
+ ### Dialects
117
+
118
+ ```ts
119
+ import { configQueryBuilder } from 'query-builder-prisma';
120
+
121
+ configQueryBuilder({ dialect: 'postgresql' }); // global default
122
+ new QueryBuilder(prisma.user, { dialect: 'mysql' }); // per-builder override (recommended)
123
+ ```
124
+
125
+ `search()` emits `mode: 'insensitive'` only on PostgreSQL / CockroachDB /
126
+ MongoDB (it throws on MySQL, SQLite, SQL Server); elsewhere the database's
127
+ collation or Prisma's limits apply. Unknown dialect names resolve to
128
+ `'universal'` — arguments valid on every database.
129
+
130
+ ## Behavior contracts
131
+
132
+ Semantics you can build on (the tests pin these):
133
+
134
+ - **Precedence** — explicit methods always win over `custom()` args, regardless of
135
+ call order: `.select(X)` beats `custom({ select: Y })`;
136
+ `.paginate(...)`/`.cursorPaginate(...)` beat `custom({ take })`;
137
+ `.sortBy()`/`.orderBy()` beat `custom({ orderBy })`.
138
+ - **`where` assembly** — conditions from `.search/.filter/.range/.where` are
139
+ AND-ed, a seeded `initialArgs.where` is AND-ed in front of them, and
140
+ `.custom({ where })` is AND-ed with the seed and previous custom `where`s —
141
+ it never replaces them.
142
+ - **`custom()` strips `undefined`** — at any depth, so
143
+ `custom({ distinct: undefined })` cannot overwrite an earlier value. Keys
144
+ named `__proto__`, `constructor` or `prototype` are refused (never written)
145
+ in `custom()`, `filter()` and dot-path expansion.
146
+ - **Sorting** — `.sortBy()` is single-key sugar and expands dot paths like
147
+ `.range()` (`'avatar.size'` → `{ avatar: { size: 'desc' } }`); `.orderBy()`
148
+ takes the full Prisma shape verbatim (arrays for multi-key, nested relation
149
+ orders). The latest of the two calls wins.
150
+ - **Cursor beats offset** — when both `.paginate()` and `.cursorPaginate()` are
151
+ configured, cursor mode wins. `executeWithCursor()` always returns rows in the
152
+ configured sort order, regardless of `direction`.
153
+ - **`executeWithMeta()` is offset-mode only** — it throws if `.cursorPaginate()`
154
+ was configured (offset meta cannot describe a seek query); use
155
+ `.executeWithCursor()` in cursor mode.
156
+ - **Cursor serialization** — `cursorPaginate({ cursor })` accepts a string,
157
+ number or `Date`; `Date` values (and `Date` cursor fields) serialize to
158
+ ISO-8601, so cursors are round-trippable and locale/TZ-independent.
159
+ - **Cursor flags are optimistic by default** — `hasNextPage`/`hasPrevPage` come
160
+ from a one-row over-fetch and can rarely claim a page exists that turns out
161
+ empty. `cursorPaginate({ strict: true })` spends one extra 1-row probe query
162
+ to make both flags exact.
163
+ - **Cursor projection guard** — `executeWithCursor()` throws when a `.select()`
164
+ projection drops `cursorField` (cursors would be built from `undefined`).
165
+ - **Clamping** — `limit` is clamped silently to `[1, 1000]`; pages below 1
166
+ become page 1; computed `skip` is capped at `100_000` (deep access → cursor
167
+ pagination). With `new QueryBuilder(delegate, { strictNumbers: true })` all of
168
+ these throw instead of clamping.
169
+ - **`includeTotal`** — `executeWithMeta({ includeTotal: false })` skips the
170
+ `COUNT` query; `meta` then carries `total: null` / `totalPage: null`.
171
+ - **Defaults** — `executeWithMeta()` without pagination defaults to page 1,
172
+ limit 10 so `meta` always describes the returned data.
173
+ - **`.first()`** — forces `take: 1` and drops `skip`/`cursor`: it returns the
174
+ first row of the whole result set, not of a configured page.
175
+ - **`search()`** — pass scalar string fields only (relation keys compile but
176
+ throw at Prisma runtime); empty or whitespace-only `searchText` adds no
177
+ condition; each `OR` arm gets its own match object.
178
+ - **`range()` reaches to-one paths only** — dot paths expand for to-one
179
+ relations and nested objects; filtering a to-many relation needs
180
+ `.custom({ where: { posts: { some: { ... } } } })`.
181
+ - **`_count` is typed wide** — `include({ _count: true })` widens the result
182
+ type with `_count: Record<string, number>` (present, correct keys are unknown
183
+ statically); the `prisma.user.findMany(qb.buildArgs())` passthrough recovers
184
+ the exact per-relation shape.
185
+ - **`cursorField`** — must be unique and sequentially ordered (pair it with a
186
+ matching `.sortBy()`); duplicates paginate wrong silently.
187
+ - **One builder per query** — instances are cheap; never share one across
188
+ concurrent requests. Framework recipes: [docs/framework-recipes.md](./docs/framework-recipes.md).
189
+ - **Dialect config** — global config state is shared via `globalThis`, so ESM+CJS
190
+ dual loading sees one config; the per-builder `dialect` option remains the
191
+ recommended path, and unknown names resolve to `'universal'` (name included).
192
+
193
+ The former HIGH-severity footguns (seed-`where` replacement, `executeWithMeta()`
194
+ under cursor mode, nested-include type narrowing) are fixed and
195
+ regression-tested. Remaining limitations are documented below.
196
+
197
+ ## Known limitations
198
+
199
+ Open and documented rather than hidden — none produces a silently wrong result:
200
+
201
+ - **Plain `.execute()` after `.cursorPaginate()`** returns raw seek-order rows
202
+ including the one-row over-fetch — use `.executeWithCursor()` for trimmed,
203
+ sort-ordered pages (kept as raw-passthrough semantics by design; the
204
+ `buildArgs()` passthrough is always available).
205
+
206
+ ## FAQ
207
+
208
+ **Does this need Prisma installed?**
209
+ The package never imports Prisma, but of course you query *through* your
210
+ Prisma delegates — you pass them in. Any object with the `findMany` + `count`
211
+ call shape works, which is also how the test suite proves the decoupling.
212
+
213
+ **Which Prisma versions are supported?**
214
+ Every version whose model delegates keep the standard call shape — 5, 6, 7, 8
215
+ today, future majors by design. There is nothing to update on a Prisma
216
+ upgrade.
217
+
218
+ **Can I share one builder across requests?**
219
+ No — one builder per query. Instances are cheap; a shared instance mixes
220
+ conditions between concurrent requests. See
221
+ [Advanced patterns](./docs/guides/advanced.md#one-builder-per-query) and the
222
+ [framework recipes](./docs/framework-recipes.md).
223
+
224
+ **What if a type isn't narrow enough?**
225
+ The engine degrades wider, never wrong (today only `_count`). For exact
226
+ Prisma-native inference on any query, use the passthrough:
227
+ `prisma.user.findMany(qb.buildArgs())`.
228
+
229
+ **How do I add a database Prisma adds later?**
230
+ One `registerDialect(name, capabilities)` call — no core changes. See
231
+ [Dialects](./docs/guides/dialects.md).
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ npm run verify # lint + typecheck + test + build (the bar for every change)
237
+ npm test # unit tests — fake delegates, zero Prisma required
238
+ ```
239
+
240
+ - `src/core` — builder, condition assembly, pagination math
241
+ - `src/dialects` — capability registry, per-DB capabilities, auto-detect hook
242
+ - `src/types` — structural delegate contracts + projection type engine
243
+ - `test/types` — compile-time projection tests (run by `npm run typecheck`)
244
+ - `builder-old/` — the original prototype this package was extracted from (reference only)
245
+
246
+ ### Links
247
+
248
+ - [ARCHITECTURE.md](./ARCHITECTURE.md) — design, boundaries, invariants
249
+ - [docs/index.md](./docs/index.md) — full documentation hub
250
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) — how to contribute
251
+ - [SECURITY.md](./SECURITY.md) — vulnerability reporting
252
+ - [CHANGELOG.md](./CHANGELOG.md) — release history
253
+
254
+ > Published on npm as [`query-builder-prisma`](https://www.npmjs.com/package/query-builder-prisma).
255
+ > MIT licensed.