prisma-flare 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/dist/cli/db-create.cjs +240 -0
- package/dist/cli/db-create.d.cts +1 -0
- package/dist/cli/db-create.d.ts +1 -0
- package/dist/cli/db-create.js +217 -0
- package/dist/cli/db-drop.cjs +263 -0
- package/dist/cli/db-drop.d.cts +1 -0
- package/dist/cli/db-drop.d.ts +1 -0
- package/dist/cli/db-drop.js +240 -0
- package/dist/cli/db-migrate.cjs +318 -0
- package/dist/cli/db-migrate.d.cts +1 -0
- package/dist/cli/db-migrate.d.ts +1 -0
- package/dist/cli/db-migrate.js +295 -0
- package/dist/cli/db-reset.cjs +110 -0
- package/dist/cli/db-reset.d.cts +1 -0
- package/dist/cli/db-reset.d.ts +1 -0
- package/dist/cli/db-reset.js +87 -0
- package/dist/cli/db-seed.cjs +87 -0
- package/dist/cli/db-seed.d.cts +1 -0
- package/dist/cli/db-seed.d.ts +1 -0
- package/dist/cli/db-seed.js +64 -0
- package/dist/cli/index.cjs +352 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +328 -0
- package/dist/core/flareBuilder.cjs +681 -0
- package/dist/core/flareBuilder.d.cts +402 -0
- package/dist/core/flareBuilder.d.ts +402 -0
- package/dist/core/flareBuilder.js +658 -0
- package/dist/core/hooks.cjs +243 -0
- package/dist/core/hooks.d.cts +13 -0
- package/dist/core/hooks.d.ts +13 -0
- package/dist/core/hooks.js +209 -0
- package/dist/generated.cjs +31 -0
- package/dist/generated.d.cts +4 -0
- package/dist/generated.d.ts +4 -0
- package/dist/generated.js +6 -0
- package/dist/index.cjs +1315 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +1261 -0
- package/dist/prisma.types-nGNe1CG8.d.cts +201 -0
- package/dist/prisma.types-nGNe1CG8.d.ts +201 -0
- package/license.md +21 -0
- package/package.json +115 -0
- package/readme.md +957 -0
package/readme.md
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
# Prisma Flare
|
|
2
|
+
|
|
3
|
+
A powerful TypeScript utilities package for Prisma ORM that provides a callback system and a query builder for chained operations.
|
|
4
|
+
|
|
5
|
+
## Performance
|
|
6
|
+
|
|
7
|
+
Prisma Flare adds **virtually zero overhead** to your queries. Our rigorous benchmarks show:
|
|
8
|
+
|
|
9
|
+
| Query Type | Prisma | Flare | Overhead |
|
|
10
|
+
|------------|--------|-------|----------|
|
|
11
|
+
| findFirst by ID | 0.083ms | 0.083ms | +0.25% |
|
|
12
|
+
| findFirst + include | 0.202ms | 0.202ms | +0.23% |
|
|
13
|
+
| COUNT with WHERE | 0.091ms | 0.091ms | +0.34% |
|
|
14
|
+
| Complex query (WHERE + ORDER + LIMIT + INCLUDE) | 0.331ms | 0.332ms | +0.38% |
|
|
15
|
+
| Custom model methods in include | 0.940ms | 0.942ms | +0.14% |
|
|
16
|
+
|
|
17
|
+
**Median overhead: 0.1% - 0.4%** (~0.001ms per query)
|
|
18
|
+
|
|
19
|
+
<details>
|
|
20
|
+
<summary><b>Benchmark Methodology</b></summary>
|
|
21
|
+
|
|
22
|
+
- **500 iterations** per test with **50 warmup iterations** for connection pool
|
|
23
|
+
- **Random alternating execution** between Prisma and Flare to eliminate ordering bias
|
|
24
|
+
- **Statistical measures**: median, p95, standard deviation (median used for comparison)
|
|
25
|
+
- **Test data**: 10 users, 200 posts with realistic field values
|
|
26
|
+
- **Database**: SQLite (results are consistent across PostgreSQL/MySQL)
|
|
27
|
+
|
|
28
|
+
What Flare adds:
|
|
29
|
+
- Object instantiation: ~0.001ms (FlareBuilder class)
|
|
30
|
+
- Method chaining: ~0.001ms per method call
|
|
31
|
+
- Model registry lookup: ~0.001ms (Map.get for includes with custom methods)
|
|
32
|
+
|
|
33
|
+
Run benchmarks yourself:
|
|
34
|
+
```bash
|
|
35
|
+
npm test -- --grep "Benchmark"
|
|
36
|
+
```
|
|
37
|
+
</details>
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Plug & Play**: Works with any existing Prisma project
|
|
42
|
+
- **Flare Builder**: Elegant chainable query API for Prisma models
|
|
43
|
+
- **Auto-Generated Queries**: Automatically generates query classes based on your schema
|
|
44
|
+
- **Callback System**: Hooks for before/after operations (create, update, delete) and after upsert
|
|
45
|
+
- **Column-Level Hooks**: Track changes to specific columns with `afterChange` callbacks
|
|
46
|
+
- **Extended Prisma Client**: Enhanced PrismaClient with additional utility methods
|
|
47
|
+
- **Type-Safe**: Full IntelliSense and compile-time type checking
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install prisma-flare
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Ensure you have `@prisma/client` installed as a peer dependency.
|
|
56
|
+
|
|
57
|
+
### Prisma Version Compatibility
|
|
58
|
+
|
|
59
|
+
| Prisma Version | prisma-flare Support |
|
|
60
|
+
|----------------|----------------------|
|
|
61
|
+
| 5.x | ✅ Full support |
|
|
62
|
+
| 6.x | ✅ Full support |
|
|
63
|
+
| 7.x+ | ✅ Full support |
|
|
64
|
+
|
|
65
|
+
prisma-flare automatically detects your Prisma version at runtime and uses the appropriate API:
|
|
66
|
+
- **Prisma ≤6**: Uses the legacy `$use()` middleware API
|
|
67
|
+
- **Prisma 7+**: Uses the new client extensions API
|
|
68
|
+
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
### 1. Initialize your Client
|
|
72
|
+
|
|
73
|
+
Replace your standard `PrismaClient` with `FlareClient` in your database setup file (e.g., `src/db.ts` or `src/lib/prisma.ts`).
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// src/db.ts
|
|
77
|
+
import { FlareClient, registerHooks } from 'prisma-flare';
|
|
78
|
+
|
|
79
|
+
// Initialize hooks middleware and auto-load callbacks
|
|
80
|
+
export const db = await registerHooks(new FlareClient());
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`registerHooks()` is async and:
|
|
84
|
+
- Registers the hooks middleware (using the appropriate API for your Prisma version)
|
|
85
|
+
- Automatically loads all callback files from `prisma/callbacks` (or your configured path)
|
|
86
|
+
- Returns the extended client instance
|
|
87
|
+
|
|
88
|
+
### 2. Generate Query Classes
|
|
89
|
+
|
|
90
|
+
Run the generator to create type-safe query classes for your specific schema.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx prisma-flare generate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
By default, this will look for your `db` instance in `src/db` and output queries to `src/models`.
|
|
97
|
+
|
|
98
|
+
### 3. Configuration (Optional)
|
|
99
|
+
|
|
100
|
+
If your project structure is different, create a `prisma-flare.config.json` in your project root:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"modelsPath": "src/models",
|
|
105
|
+
"dbPath": "src/lib/db",
|
|
106
|
+
"callbacksPath": "src/callbacks",
|
|
107
|
+
"envPath": ".env.local"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `modelsPath`: Where to generate the query classes (defaults to `prisma/models`).
|
|
112
|
+
- `dbPath`: Path to the file exporting your `db` instance (relative to project root, defaults to `prisma/db`).
|
|
113
|
+
- `callbacksPath`: Directory containing your callback/hook files (defaults to `prisma/callbacks`). All `.ts`/`.js` files in this directory are automatically loaded when `registerHooks()` is called.
|
|
114
|
+
- `envPath`: Path to your environment file (optional, defaults to `.env`).
|
|
115
|
+
- `plurals`: Custom pluralization for model names (optional).
|
|
116
|
+
|
|
117
|
+
Example with custom plurals:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"plurals": {
|
|
122
|
+
"Person": "people",
|
|
123
|
+
"Equipment": "equipment"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
### Flare Builder
|
|
131
|
+
|
|
132
|
+
Once generated, you can import the `DB` class to access chainable methods for your models.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { DB } from 'prisma-flare/generated';
|
|
136
|
+
|
|
137
|
+
// Chainable query builder with full type safety
|
|
138
|
+
const posts = await DB.posts
|
|
139
|
+
.where({ published: true })
|
|
140
|
+
.order({ createdAt: 'desc' })
|
|
141
|
+
.limit(10)
|
|
142
|
+
.include({ author: true })
|
|
143
|
+
.findMany();
|
|
144
|
+
|
|
145
|
+
// Complex filtering made easy
|
|
146
|
+
const activeUsers = await DB.users
|
|
147
|
+
.where({ isActive: true })
|
|
148
|
+
.where({ role: 'ADMIN' })
|
|
149
|
+
.count();
|
|
150
|
+
|
|
151
|
+
// Pagination
|
|
152
|
+
const { data, meta } = await DB.users.paginate(1, 15);
|
|
153
|
+
|
|
154
|
+
// Conditional queries
|
|
155
|
+
const search = 'John';
|
|
156
|
+
const users = await DB.users
|
|
157
|
+
.when(!!search, (q) => q.where({ name: { contains: search } }))
|
|
158
|
+
.findMany();
|
|
159
|
+
|
|
160
|
+
// Access raw Prisma Client instance
|
|
161
|
+
const rawDb = DB.instance;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Transactions
|
|
165
|
+
|
|
166
|
+
Prisma Flare provides a powerful wrapper around Prisma's interactive transactions, allowing you to use the fluent `from()` API within a transaction scope.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Simple transaction
|
|
170
|
+
const result = await DB.instance.transaction(async (tx) => {
|
|
171
|
+
// Create a user
|
|
172
|
+
const user = await tx.from('user').create({
|
|
173
|
+
email: 'tx-user@example.com',
|
|
174
|
+
name: 'Transaction User',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Create a related post using the user's ID
|
|
178
|
+
const post = await tx.from('post').create({
|
|
179
|
+
title: 'Transaction Post',
|
|
180
|
+
content: 'Content',
|
|
181
|
+
authorId: user.id,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { user, post };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Complex logic with conditional operations
|
|
188
|
+
await DB.instance.transaction(async (tx) => {
|
|
189
|
+
const existing = await tx.from('user').where({ email: 'check@example.com' }).findFirst();
|
|
190
|
+
|
|
191
|
+
if (!existing) {
|
|
192
|
+
await tx.from('user').create({
|
|
193
|
+
email: 'check@example.com',
|
|
194
|
+
name: 'New User'
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
await tx.from('user').withId(existing.id).update({
|
|
198
|
+
lastLogin: new Date()
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Callhooks & Middleware
|
|
205
|
+
|
|
206
|
+
Define hooks to run logic before or after database operations. Create callback files in your callbacks directory (default: `prisma/callbacks`) and they'll be automatically loaded.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// prisma/callbacks/user.ts
|
|
210
|
+
import { beforeCreate, afterCreate } from 'prisma-flare';
|
|
211
|
+
|
|
212
|
+
// Validation: Prevent creating users with invalid emails
|
|
213
|
+
beforeCreate('user', async (args) => {
|
|
214
|
+
if (!args.data.email.includes('@')) {
|
|
215
|
+
throw new Error('Invalid email address');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Run after a user is created
|
|
220
|
+
afterCreate('user', async (args, result) => {
|
|
221
|
+
console.log('New user created:', result.email);
|
|
222
|
+
await sendWelcomeEmail(result.email);
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// prisma/callbacks/post.ts
|
|
228
|
+
import { afterChange } from 'prisma-flare';
|
|
229
|
+
|
|
230
|
+
// Run when the 'published' field on post changes
|
|
231
|
+
afterChange('post', 'published', async (oldValue, newValue, record) => {
|
|
232
|
+
if (!oldValue && newValue) {
|
|
233
|
+
console.log(`Post "${record.title}" was published!`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
All files in the callbacks directory are automatically imported when `registerHooks()` is called. No manual loading required.
|
|
239
|
+
|
|
240
|
+
#### Hook Configuration
|
|
241
|
+
|
|
242
|
+
Configure hook behavior globally, especially useful for performance tuning:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { hookRegistry } from 'prisma-flare';
|
|
246
|
+
|
|
247
|
+
// Disable column hooks globally (for performance-critical paths)
|
|
248
|
+
hookRegistry.configure({ enableColumnHooks: false });
|
|
249
|
+
|
|
250
|
+
// Limit re-fetching on large updateMany operations
|
|
251
|
+
// Column hooks will be skipped if more than 1000 records are affected
|
|
252
|
+
hookRegistry.configure({ maxRefetch: 1000 });
|
|
253
|
+
|
|
254
|
+
// Disable the warning when hooks are skipped
|
|
255
|
+
hookRegistry.configure({ warnOnSkip: false });
|
|
256
|
+
|
|
257
|
+
// Check current configuration
|
|
258
|
+
const config = hookRegistry.getConfig();
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Configuration options:**
|
|
262
|
+
|
|
263
|
+
| Option | Default | Description |
|
|
264
|
+
|--------|---------|-------------|
|
|
265
|
+
| `enableColumnHooks` | `true` | Enable/disable all column-level hooks |
|
|
266
|
+
| `maxRefetch` | `1000` | Max records to re-fetch for column hooks. Prevents expensive operations on large `updateMany`. Set to `Infinity` to disable limit. |
|
|
267
|
+
| `warnOnSkip` | `true` | Log warning when hooks are skipped due to limits |
|
|
268
|
+
|
|
269
|
+
#### Per-Call Hook Skip
|
|
270
|
+
|
|
271
|
+
For fine-grained control, you can skip column hooks on a per-call basis without changing global configuration:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Skip column hooks for this specific update only
|
|
275
|
+
await DB.users.withId(userId).update({
|
|
276
|
+
status: 'active',
|
|
277
|
+
// This meta key is stripped before reaching Prisma
|
|
278
|
+
__flare: { skipColumnHooks: true }
|
|
279
|
+
} as any);
|
|
280
|
+
|
|
281
|
+
// Regular hooks (beforeUpdate, afterUpdate) still fire
|
|
282
|
+
// Only column-level hooks (afterChange) are skipped
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This is useful for:
|
|
286
|
+
- Batch migrations where you don't want to trigger side effects
|
|
287
|
+
- Performance-critical paths where you know the column change doesn't matter
|
|
288
|
+
- Avoiding recursive hook triggers
|
|
289
|
+
|
|
290
|
+
#### Smart Value Comparison
|
|
291
|
+
|
|
292
|
+
Column hooks use intelligent comparison to detect real changes:
|
|
293
|
+
|
|
294
|
+
| Type | Comparison Method |
|
|
295
|
+
|------|-------------------|
|
|
296
|
+
| `Date` | Compares by `.getTime()` (milliseconds) |
|
|
297
|
+
| `Decimal` (Prisma) | Compares by `.toString()` |
|
|
298
|
+
| `null` / `undefined` | Strict equality |
|
|
299
|
+
| Objects/JSON | Deep comparison via `JSON.stringify` |
|
|
300
|
+
| Primitives | Strict equality (`===`) |
|
|
301
|
+
|
|
302
|
+
This prevents false positives when:
|
|
303
|
+
- Dates are re-assigned but represent the same moment
|
|
304
|
+
- Decimal values are equivalent but different instances
|
|
305
|
+
- JSON fields are structurally identical
|
|
306
|
+
|
|
307
|
+
#### Advanced Hook Registration
|
|
308
|
+
|
|
309
|
+
For more control over hook registration, prisma-flare exports additional utilities:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import {
|
|
313
|
+
registerHooks, // Auto-detects Prisma version + auto-loads callbacks (recommended)
|
|
314
|
+
registerHooksLegacy, // Force legacy $use API (Prisma ≤6 only, no auto-load)
|
|
315
|
+
createHooksExtension, // Get raw extension for manual use
|
|
316
|
+
loadCallbacks // Manually load callbacks from a custom path
|
|
317
|
+
} from 'prisma-flare';
|
|
318
|
+
|
|
319
|
+
// Option 1: Auto-detect with auto-loading (recommended)
|
|
320
|
+
const db = await registerHooks(new FlareClient());
|
|
321
|
+
|
|
322
|
+
// Option 2: Manual callback loading from custom path
|
|
323
|
+
import { PrismaClient } from '@prisma/client';
|
|
324
|
+
const prisma = new PrismaClient().$extends(createHooksExtension(new PrismaClient()));
|
|
325
|
+
await loadCallbacks('/custom/path/to/callbacks');
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## CLI Utilities
|
|
329
|
+
|
|
330
|
+
Prisma Flare comes with a suite of CLI tools to manage your database workflow. It supports **PostgreSQL** and **SQLite** out of the box, and is extensible for other databases.
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
npx prisma-flare generate # Generate query classes from schema
|
|
334
|
+
npx prisma-flare create # Create database
|
|
335
|
+
npx prisma-flare drop # Drop database
|
|
336
|
+
npx prisma-flare migrate # Run migrations
|
|
337
|
+
npx prisma-flare reset # Reset database
|
|
338
|
+
npx prisma-flare seed # Seed database
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Custom Database Adapters
|
|
342
|
+
|
|
343
|
+
You can add support for other databases by registering a custom adapter.
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { dbAdapterRegistry, DatabaseAdapter } from 'prisma-flare';
|
|
347
|
+
|
|
348
|
+
const myAdapter: DatabaseAdapter = {
|
|
349
|
+
name: 'my-db',
|
|
350
|
+
matches: (url) => url.startsWith('mydb://'),
|
|
351
|
+
create: async (url) => { /* custom create logic */ },
|
|
352
|
+
drop: async (url) => { /* custom drop logic */ }
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
dbAdapterRegistry.register(myAdapter);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Custom Query Methods
|
|
359
|
+
|
|
360
|
+
You can extend the generated query classes with custom methods for your domain-specific needs. Simply add methods to your query class that use the built-in `where()` method to build conditions.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// src/models/Post.ts
|
|
364
|
+
import { db } from './db';
|
|
365
|
+
import { FlareBuilder } from 'prisma-flare';
|
|
366
|
+
|
|
367
|
+
export default class Post extends FlareBuilder<'post'> {
|
|
368
|
+
constructor() {
|
|
369
|
+
super(db.post);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Filter published posts
|
|
373
|
+
published(): this {
|
|
374
|
+
this.where({ published: true });
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Filter draft posts
|
|
379
|
+
drafts(): this {
|
|
380
|
+
this.where({ published: false });
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Search by title (contains)
|
|
385
|
+
withTitle(title: string): this {
|
|
386
|
+
this.where({ title: { contains: title } });
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Filter by author ID
|
|
391
|
+
withAuthorId(authorId: number): this {
|
|
392
|
+
this.where({ authorId });
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Get recent posts
|
|
397
|
+
recent(days: number): this {
|
|
398
|
+
const date = new Date();
|
|
399
|
+
date.setDate(date.getDate() - days);
|
|
400
|
+
this.where({ createdAt: { gte: date } });
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Then use your custom methods in queries:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { DB } from 'prisma-flare/generated';
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
// Use custom methods with full chainability
|
|
413
|
+
const recentPublished = await DB.post
|
|
414
|
+
.published()
|
|
415
|
+
.recent(7)
|
|
416
|
+
.order({ createdAt: 'desc' })
|
|
417
|
+
.findMany();
|
|
418
|
+
|
|
419
|
+
const authorPosts = await DB.post
|
|
420
|
+
.withAuthorId(123)
|
|
421
|
+
.withTitle('TypeScript')
|
|
422
|
+
.include({ author: true })
|
|
423
|
+
.findMany();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Tips for Custom Methods:**
|
|
427
|
+
- Always return `this` to maintain chainability
|
|
428
|
+
- Use descriptive names with prefixes like `with*` for filters
|
|
429
|
+
- Leverage Prisma's query operators (`contains`, `gte`, `lte`, etc.)
|
|
430
|
+
- Keep methods focused on a single responsibility
|
|
431
|
+
|
|
432
|
+
## Flare Builder API Reference
|
|
433
|
+
|
|
434
|
+
### Query Building Methods
|
|
435
|
+
|
|
436
|
+
These methods build and customize your query before execution.
|
|
437
|
+
|
|
438
|
+
#### `where(condition)`
|
|
439
|
+
Adds a WHERE condition to the query with full type safety from Prisma.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Single condition
|
|
443
|
+
const users = await DB.users.where({ isActive: true }).findMany();
|
|
444
|
+
|
|
445
|
+
// Multiple conditions (merged together)
|
|
446
|
+
const users = await DB.users
|
|
447
|
+
.where({ isActive: true })
|
|
448
|
+
.where({ role: 'ADMIN' })
|
|
449
|
+
.findMany();
|
|
450
|
+
|
|
451
|
+
// Complex conditions with operators
|
|
452
|
+
const users = await DB.users.where({
|
|
453
|
+
email: { contains: 'example.com' },
|
|
454
|
+
age: { gte: 18 }
|
|
455
|
+
}).findMany();
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Boolean Logic (AND/OR/NOT)
|
|
459
|
+
|
|
460
|
+
Prisma Flare provides explicit control over boolean logic. Understanding how conditions compose is critical for correct queries.
|
|
461
|
+
|
|
462
|
+
#### How `where()` chaining works
|
|
463
|
+
|
|
464
|
+
Multiple `where()` calls are composed using **AND** logic:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// These are equivalent:
|
|
468
|
+
DB.users.where({ status: 'active' }).where({ role: 'admin' })
|
|
469
|
+
// → { AND: [{ status: 'active' }, { role: 'admin' }] }
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### `orWhere(condition)` - ⚠️ Advanced
|
|
473
|
+
|
|
474
|
+
`orWhere()` wraps the **entire accumulated where** in an OR:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
DB.users.where({ status: 'active' }).orWhere({ role: 'admin' })
|
|
478
|
+
// → { OR: [{ status: 'active' }, { role: 'admin' }] }
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**⚠️ Common Mistake:** Adding more conditions after `orWhere` can produce unexpected results:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// ❌ WRONG: User thinks "active users named Alice or Bob"
|
|
485
|
+
const wrong = await DB.users
|
|
486
|
+
.where({ status: 'active' })
|
|
487
|
+
.where({ name: 'Alice' })
|
|
488
|
+
.orWhere({ name: 'Bob' }) // This OR-wraps EVERYTHING before it!
|
|
489
|
+
.findMany();
|
|
490
|
+
// Actual: (status='active' AND name='Alice') OR (name='Bob')
|
|
491
|
+
// Bob is included even if inactive!
|
|
492
|
+
|
|
493
|
+
// ✅ CORRECT: Use whereGroup for explicit grouping
|
|
494
|
+
const correct = await DB.users
|
|
495
|
+
.where({ status: 'active' })
|
|
496
|
+
.whereGroup(qb => qb
|
|
497
|
+
.where({ name: 'Alice' })
|
|
498
|
+
.orWhere({ name: 'Bob' })
|
|
499
|
+
)
|
|
500
|
+
.findMany();
|
|
501
|
+
// Result: status='active' AND (name='Alice' OR name='Bob')
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
For complex logic, **always prefer `whereGroup()`** for explicit control.
|
|
505
|
+
|
|
506
|
+
#### `whereGroup(callback)` - Recommended for complex logic
|
|
507
|
+
|
|
508
|
+
Creates an explicit group that's AND-ed with the existing where:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// (status = 'active') AND (role = 'admin' OR role = 'moderator')
|
|
512
|
+
const users = await DB.users
|
|
513
|
+
.where({ status: 'active' })
|
|
514
|
+
.whereGroup(qb => qb
|
|
515
|
+
.where({ role: 'admin' })
|
|
516
|
+
.orWhere({ role: 'moderator' })
|
|
517
|
+
)
|
|
518
|
+
.findMany();
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### `orWhereGroup(callback)`
|
|
522
|
+
|
|
523
|
+
Creates an explicit group that's OR-ed with the existing where:
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
// (status = 'active') OR (role = 'admin' AND verified = true)
|
|
527
|
+
const users = await DB.users
|
|
528
|
+
.where({ status: 'active' })
|
|
529
|
+
.orWhereGroup(qb => qb
|
|
530
|
+
.where({ role: 'admin' })
|
|
531
|
+
.where({ verified: true })
|
|
532
|
+
)
|
|
533
|
+
.findMany();
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### NOT conditions
|
|
537
|
+
|
|
538
|
+
Use Prisma's `NOT` operator inside `where()`:
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// Active users who are NOT banned
|
|
542
|
+
const users = await DB.users
|
|
543
|
+
.where({ status: 'active' })
|
|
544
|
+
.where({ NOT: { role: 'banned' } })
|
|
545
|
+
.findMany();
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### Quick Reference
|
|
549
|
+
|
|
550
|
+
| Pattern | Result |
|
|
551
|
+
|---------|--------|
|
|
552
|
+
| `.where(A).where(B)` | `A AND B` |
|
|
553
|
+
| `.where(A).orWhere(B)` | `A OR B` |
|
|
554
|
+
| `.where(A).orWhere(B).where(C)` | `(A OR B) AND C` |
|
|
555
|
+
| `.where(A).whereGroup(q => q.where(B).orWhere(C))` | `A AND (B OR C)` |
|
|
556
|
+
| `.where(A).orWhereGroup(q => q.where(B).where(C))` | `A OR (B AND C)` |
|
|
557
|
+
|
|
558
|
+
**Rule of thumb:** For anything beyond simple AND chains or single OR, use `whereGroup()`/`orWhereGroup()`.
|
|
559
|
+
|
|
560
|
+
#### `withId(id)`
|
|
561
|
+
Filters records by ID. Throws an error if no ID is provided.
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
const user = await DB.users.withId(123).findFirst();
|
|
565
|
+
const post = await DB.posts.withId('uuid-string').findUnique();
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
#### `select(fields)`
|
|
569
|
+
Selects specific fields to retrieve. Reduces data transfer and improves query performance.
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// Select only name and email
|
|
573
|
+
const users = await DB.users
|
|
574
|
+
.select({ id: true, name: true, email: true })
|
|
575
|
+
.findMany();
|
|
576
|
+
|
|
577
|
+
// Combine with other conditions
|
|
578
|
+
const admin = await DB.users
|
|
579
|
+
.where({ role: 'ADMIN' })
|
|
580
|
+
.select({ id: true, email: true })
|
|
581
|
+
.findFirst();
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
#### `include(relation)` or `include(relation, callback)`
|
|
585
|
+
Includes related records in the query. Can be called multiple times for nested relations.
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
// Include all default fields from the relation
|
|
589
|
+
const posts = await DB.posts
|
|
590
|
+
.include('author')
|
|
591
|
+
.findMany();
|
|
592
|
+
|
|
593
|
+
// Include with custom query on the relation
|
|
594
|
+
const posts = await DB.posts
|
|
595
|
+
.include('author', (q) =>
|
|
596
|
+
q.select({ id: true, name: true, email: true })
|
|
597
|
+
)
|
|
598
|
+
.findMany();
|
|
599
|
+
|
|
600
|
+
// Multiple includes
|
|
601
|
+
const posts = await DB.posts
|
|
602
|
+
.include('author')
|
|
603
|
+
.include('comments', (q) => q.limit(5))
|
|
604
|
+
.findMany();
|
|
605
|
+
|
|
606
|
+
// Nested includes
|
|
607
|
+
const posts = await DB.posts
|
|
608
|
+
.include('author', (q) =>
|
|
609
|
+
q.include('profile')
|
|
610
|
+
)
|
|
611
|
+
.findMany();
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
#### `order(orderBy)`
|
|
615
|
+
Adds ordering to the query results.
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Single field ascending
|
|
619
|
+
const users = await DB.users.order({ createdAt: 'asc' }).findMany();
|
|
620
|
+
|
|
621
|
+
// Single field descending
|
|
622
|
+
const posts = await DB.posts.order({ published: 'desc' }).findMany();
|
|
623
|
+
|
|
624
|
+
// Multiple fields
|
|
625
|
+
const comments = await DB.comments
|
|
626
|
+
.order({ likes: 'desc', createdAt: 'asc' })
|
|
627
|
+
.findMany();
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
#### `first(key?)` and `last(key?)`
|
|
631
|
+
Convenience methods to get the first or last record. Automatically sets limit to 1.
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// Get the first user (by createdAt)
|
|
635
|
+
const first = await DB.users.first().findFirst();
|
|
636
|
+
|
|
637
|
+
// Get the last post (by date)
|
|
638
|
+
const latest = await DB.posts.last('publishedAt').findFirst();
|
|
639
|
+
|
|
640
|
+
// Chain with where conditions
|
|
641
|
+
const first = await DB.posts
|
|
642
|
+
.where({ published: true })
|
|
643
|
+
.first()
|
|
644
|
+
.findFirst();
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### `limit(n)`
|
|
648
|
+
Limits the number of records returned.
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
const topTen = await DB.posts.limit(10).findMany();
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
#### `skip(offset)`
|
|
655
|
+
Skips a number of records (useful for custom pagination).
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
const page = await DB.users.skip(20).limit(10).findMany();
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
#### `distinct(fields)`
|
|
662
|
+
Returns only distinct records based on the specified fields.
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
// Get distinct user emails
|
|
666
|
+
const distinctEmails = await DB.users
|
|
667
|
+
.distinct({ email: true })
|
|
668
|
+
.select({ email: true })
|
|
669
|
+
.findMany();
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
#### `groupBy(fields)`
|
|
673
|
+
Groups results by the specified fields (aggregation).
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
const grouped = await DB.posts
|
|
677
|
+
.groupBy({ authorId: true })
|
|
678
|
+
.findMany();
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
#### `having(condition)`
|
|
682
|
+
Adds a HAVING clause for aggregate queries.
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
const authors = await DB.posts
|
|
686
|
+
.groupBy({ authorId: true })
|
|
687
|
+
.having({ id: { _count: { gt: 5 } } })
|
|
688
|
+
.findMany();
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
#### `getQuery()`
|
|
692
|
+
Returns the current internal query object. Useful for debugging or passing to raw operations.
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
const query = DB.users.where({ active: true }).getQuery();
|
|
696
|
+
console.log(query);
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Execution Methods
|
|
700
|
+
|
|
701
|
+
These methods execute the query and return results.
|
|
702
|
+
|
|
703
|
+
#### `findMany()`
|
|
704
|
+
Returns all records matching the query conditions.
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
const allUsers = await DB.users.findMany();
|
|
708
|
+
const activeUsers = await DB.users.where({ isActive: true }).findMany();
|
|
709
|
+
const limited = await DB.users.limit(10).findMany();
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### `findFirst()`
|
|
713
|
+
Returns the first record matching the query, or `null` if none found.
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
const user = await DB.users.where({ email: 'user@example.com' }).findFirst();
|
|
717
|
+
if (user) {
|
|
718
|
+
console.log('User found:', user.name);
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
#### `findFirstOrThrow()`
|
|
723
|
+
Like `findFirst()`, but throws a `Prisma.NotFoundError` if no record is found.
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
try {
|
|
727
|
+
const user = await DB.users
|
|
728
|
+
.where({ email: 'admin@example.com' })
|
|
729
|
+
.findFirstOrThrow();
|
|
730
|
+
} catch (error) {
|
|
731
|
+
console.error('User not found');
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
#### `findUnique()`
|
|
736
|
+
Finds a record by a unique constraint (typically the ID). Returns `null` if not found.
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
const user = await DB.users.withId(123).findUnique();
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
#### `findUniqueOrThrow()`
|
|
743
|
+
Like `findUnique()`, but throws an error if the record is not found.
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
const user = await DB.users.withId(123).findUniqueOrThrow();
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### `create(data)`
|
|
750
|
+
Creates a new record with the provided data. Triggers any registered hooks.
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
const newUser = await DB.users.create({
|
|
754
|
+
email: 'new@example.com',
|
|
755
|
+
name: 'New User'
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
#### `createMany(data)`
|
|
760
|
+
Creates multiple records in a single operation. More efficient than individual creates.
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
const result = await DB.posts.createMany({
|
|
764
|
+
data: [
|
|
765
|
+
{ title: 'Post 1', content: 'Content 1', authorId: 1 },
|
|
766
|
+
{ title: 'Post 2', content: 'Content 2', authorId: 1 }
|
|
767
|
+
]
|
|
768
|
+
});
|
|
769
|
+
console.log(`Created ${result.count} posts`);
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
#### `update(data)`
|
|
773
|
+
Updates a single record. Requires a unique constraint (typically id) in the where condition.
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
const updated = await DB.users
|
|
777
|
+
.withId(123)
|
|
778
|
+
.update({ name: 'Updated Name' });
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
#### `updateMany(data)`
|
|
782
|
+
Updates multiple records matching the current conditions.
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
const result = await DB.users
|
|
786
|
+
.where({ status: 'inactive' })
|
|
787
|
+
.updateMany({ lastLogin: new Date() });
|
|
788
|
+
console.log(`Updated ${result.count} users`);
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
#### `delete()`
|
|
792
|
+
Deletes a single record. Requires a unique constraint in the where condition.
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
const deleted = await DB.posts.withId(123).delete();
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
#### `deleteMany()`
|
|
799
|
+
Deletes multiple records matching the current conditions.
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
const result = await DB.posts
|
|
803
|
+
.where({ published: false })
|
|
804
|
+
.deleteMany();
|
|
805
|
+
console.log(`Deleted ${result.count} drafts`);
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
#### `upsert(args)`
|
|
809
|
+
Updates a record if it exists, otherwise creates a new one.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const result = await DB.users
|
|
813
|
+
.where({ email: 'user@example.com' })
|
|
814
|
+
.upsert({
|
|
815
|
+
create: { email: 'user@example.com', name: 'New User' },
|
|
816
|
+
update: { lastLogin: new Date() }
|
|
817
|
+
});
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Aggregation Methods
|
|
821
|
+
|
|
822
|
+
These methods perform calculations on your data.
|
|
823
|
+
|
|
824
|
+
#### `count()`
|
|
825
|
+
Counts records matching the current query.
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
const totalUsers = await DB.users.count();
|
|
829
|
+
const activeCount = await DB.users.where({ isActive: true }).count();
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
#### `sum(field)`
|
|
833
|
+
Sums a numeric field across matching records.
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
const totalSales = await DB.orders.where({ status: 'completed' }).sum('amount');
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
#### `avg(field)`
|
|
840
|
+
Calculates the average of a numeric field.
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
const avgPrice = await DB.products.avg('price');
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
#### `min(field)`
|
|
847
|
+
Finds the minimum value of a field.
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
const oldest = await DB.users.min('createdAt');
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
#### `max(field)`
|
|
854
|
+
Finds the maximum value of a field.
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
const latest = await DB.posts.max('publishedAt');
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Utility Methods
|
|
861
|
+
|
|
862
|
+
These methods provide additional functionality for querying and data processing.
|
|
863
|
+
|
|
864
|
+
#### `only(field)`
|
|
865
|
+
Selects and returns only a specific field value from the first matching record.
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
const email = await DB.users.withId(123).only('email');
|
|
869
|
+
// Returns: 'user@example.com' or null
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
#### `pluck(field)`
|
|
873
|
+
Extracts a specific field from all matching records as an array.
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
const emails = await DB.users.where({ isActive: true }).pluck('email');
|
|
877
|
+
// Returns: ['user1@example.com', 'user2@example.com', ...]
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
#### `exists(key?)`
|
|
881
|
+
Checks if any record exists matching the current query.
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
const hasAdmins = await DB.users.where({ role: 'ADMIN' }).exists();
|
|
885
|
+
|
|
886
|
+
// Check for existence of a specific field
|
|
887
|
+
const hasEmail = await DB.users.where({ id: 123 }).exists('email');
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
#### `paginate(page, perPage)`
|
|
891
|
+
Returns paginated results with metadata for easy navigation.
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
const result = await DB.users.where({ isActive: true }).paginate(1, 15);
|
|
895
|
+
|
|
896
|
+
console.log(result.data); // Array of users
|
|
897
|
+
console.log(result.meta); // Pagination metadata
|
|
898
|
+
|
|
899
|
+
// Meta structure
|
|
900
|
+
{
|
|
901
|
+
total: 150, // Total records matching query
|
|
902
|
+
lastPage: 10, // Total number of pages
|
|
903
|
+
currentPage: 1, // Current page number
|
|
904
|
+
perPage: 15, // Records per page
|
|
905
|
+
prev: null, // Previous page number or null
|
|
906
|
+
next: 2 // Next page number or null
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Fetch next page
|
|
910
|
+
const nextPage = await DB.users
|
|
911
|
+
.where({ isActive: true })
|
|
912
|
+
.paginate(result.meta.next, 15);
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
#### `when(condition, callback)`
|
|
916
|
+
Conditionally applies query operations based on a boolean or function.
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
const search = req.query.search;
|
|
920
|
+
const role = req.query.role;
|
|
921
|
+
|
|
922
|
+
const users = await DB.users
|
|
923
|
+
.when(!!search, (q) => q.where({ name: { contains: search } }))
|
|
924
|
+
.when(!!role, (q) => q.where({ role }))
|
|
925
|
+
.findMany();
|
|
926
|
+
|
|
927
|
+
// With function condition
|
|
928
|
+
const users = await DB.users
|
|
929
|
+
.when(() => isAdmin(user), (q) => q.select({ id: true, email: true, role: true }))
|
|
930
|
+
.findMany();
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
#### `chunk(size, callback)`
|
|
934
|
+
Processes large datasets in chunks to avoid memory issues.
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
await DB.posts.chunk(100, async (posts) => {
|
|
938
|
+
// Process each chunk of 100 posts
|
|
939
|
+
for (const post of posts) {
|
|
940
|
+
await sendNotification(post.authorId);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
#### `clone()`
|
|
946
|
+
Creates an independent copy of the current query builder.
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
const baseQuery = DB.posts.where({ published: true });
|
|
950
|
+
|
|
951
|
+
const recent = baseQuery.clone().order({ createdAt: 'desc' }).findMany();
|
|
952
|
+
const popular = baseQuery.clone().order({ likes: 'desc' }).findMany();
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
## License
|
|
956
|
+
|
|
957
|
+
ISC
|