@fragno-dev/corpus 0.0.4 → 0.0.5

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.
@@ -0,0 +1,305 @@
1
+ # Client-side State Management
2
+
3
+ Client-side state management in Fragno uses Nanostores under the hood, providing reactive stores
4
+ that integrate with React, Vue, Svelte, and vanilla JavaScript. The `ClientBuilder` class creates
5
+ hooks and mutators for your routes.
6
+
7
+ ```typescript @fragno-imports
8
+ import {
9
+ defineFragment,
10
+ defineRoute,
11
+ defineRoutes,
12
+ type FragnoPublicClientConfig,
13
+ } from "@fragno-dev/core";
14
+ import { createClientBuilder } from "@fragno-dev/core/client";
15
+ import { computed } from "nanostores";
16
+ import { z } from "zod";
17
+ ```
18
+
19
+ ```typescript @fragno-prelude:fragment-setup
20
+ interface TodoConfig {
21
+ apiUrl: string;
22
+ }
23
+
24
+ const todoFragment = defineFragment<TodoConfig>("todos");
25
+
26
+ const routes = defineRoutes<TodoConfig>().create(() => [
27
+ defineRoute({
28
+ method: "GET",
29
+ path: "/todos",
30
+ outputSchema: z.array(z.object({ id: z.string(), text: z.string(), completed: z.boolean() })),
31
+ handler: async (_, { json }) => json([]),
32
+ }),
33
+ defineRoute({
34
+ method: "POST",
35
+ path: "/todos",
36
+ inputSchema: z.object({ text: z.string() }),
37
+ outputSchema: z.object({ id: z.string(), text: z.string(), completed: z.boolean() }),
38
+ handler: async ({ input }, { json }) => {
39
+ const data = await input.valid();
40
+ return json({ id: "todo-1", text: data.text, completed: false });
41
+ },
42
+ }),
43
+ defineRoute({
44
+ method: "PUT",
45
+ path: "/todos/:id",
46
+ inputSchema: z.object({ completed: z.boolean() }),
47
+ outputSchema: z.object({ id: z.string(), text: z.string(), completed: z.boolean() }),
48
+ handler: async ({ input }, { json }) => {
49
+ const data = await input.valid();
50
+ return json({ id: "todo-1", text: "Updated todo", completed: data.completed });
51
+ },
52
+ }),
53
+ defineRoute({
54
+ method: "DELETE",
55
+ path: "/todos/:id",
56
+ outputSchema: z.object({ success: z.boolean() }),
57
+ handler: async (_, { json }) => json({ success: true }),
58
+ }),
59
+ defineRoute({
60
+ method: "POST",
61
+ path: "/chat/stream",
62
+ inputSchema: z.object({
63
+ messages: z.array(z.object({ role: z.string(), content: z.string() })),
64
+ }),
65
+ outputSchema: z.array(z.object({ type: z.string(), delta: z.string().optional() })),
66
+ handler: async (_, { jsonStream }) =>
67
+ jsonStream(async (stream) => {
68
+ stream.write({ type: "response.output_text.delta", delta: "Hello" });
69
+ stream.write({ type: "response.output_text.delta", delta: " World" });
70
+ }),
71
+ }),
72
+ ]);
73
+ ```
74
+
75
+ ## Basic ClientBuilder Setup
76
+
77
+ Create a client builder and export hooks for your fragment's routes.
78
+
79
+ ```typescript @fragno-test:basic-client-builder
80
+ // should create a basic client builder
81
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
82
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
83
+
84
+ return {
85
+ useTodos: builder.createHook("/todos"),
86
+ };
87
+ }
88
+
89
+ const client = createTodoClient();
90
+ expect(client.useTodos).toBeDefined();
91
+ ```
92
+
93
+ ## Reading Data with createHook
94
+
95
+ Create read-only query hooks for GET routes.
96
+
97
+ ```typescript @fragno-test:create-hook
98
+ // should create a hook for GET routes
99
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
100
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
101
+
102
+ return {
103
+ useTodos: builder.createHook("/todos"),
104
+ };
105
+ }
106
+
107
+ const client = createTodoClient();
108
+ const hook = client.useTodos;
109
+
110
+ // Hook has store and query methods
111
+ expect(hook.store).toBeDefined();
112
+ expect(hook.query).toBeDefined();
113
+ ```
114
+
115
+ Users call the hook with path and query parameters. The hook returns `data`, `loading`, and `error`
116
+ properties that are reactive.
117
+
118
+ ## Mutating Data with createMutator
119
+
120
+ Create mutators for POST, PUT, DELETE routes. Returns `data`, `loading`, `error`, and a `mutate`
121
+ function.
122
+
123
+ ```typescript @fragno-test:create-mutator
124
+ // should create mutators for POST/PUT/DELETE routes
125
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
126
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
127
+
128
+ return {
129
+ useTodos: builder.createHook("/todos"),
130
+ useCreateTodo: builder.createMutator("POST", "/todos"),
131
+ useUpdateTodo: builder.createMutator("PUT", "/todos/:id"),
132
+ useDeleteTodo: builder.createMutator("DELETE", "/todos/:id"),
133
+ };
134
+ }
135
+
136
+ const client = createTodoClient();
137
+ expect(client.useCreateTodo.mutatorStore).toBeDefined();
138
+ expect(client.useUpdateTodo.mutatorStore).toBeDefined();
139
+ expect(client.useDeleteTodo.mutatorStore).toBeDefined();
140
+ ```
141
+
142
+ ## Custom Invalidation
143
+
144
+ By default, mutations invalidate the same route. Use `onInvalidate` to invalidate other routes.
145
+
146
+ ```typescript @fragno-test:custom-invalidation
147
+ // should invalidate other routes on mutation
148
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
149
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
150
+
151
+ return {
152
+ useTodos: builder.createHook("/todos"),
153
+ useCreateTodo: builder.createMutator("POST", "/todos", (invalidate) => {
154
+ invalidate("GET", "/todos", {});
155
+ }),
156
+ useUpdateTodo: builder.createMutator("PUT", "/todos/:id", (invalidate, params) => {
157
+ invalidate("GET", "/todos", {});
158
+ }),
159
+ useDeleteTodo: builder.createMutator("DELETE", "/todos/:id", (invalidate, params) => {
160
+ invalidate("GET", "/todos", {});
161
+ }),
162
+ };
163
+ }
164
+
165
+ const client = createTodoClient();
166
+ expect(client.useCreateTodo.mutatorStore).toBeDefined();
167
+ ```
168
+
169
+ ## Derived Data with Nanostores
170
+
171
+ Use `computed` to create derived stores. Must wrap with `builder.createStore` for framework
172
+ reactivity.
173
+
174
+ ```typescript @fragno-test:derived-data
175
+ // should create derived stores from mutator data
176
+ export function createChatClient(fragnoConfig: FragnoPublicClientConfig = {}) {
177
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
178
+
179
+ const chatStream = builder.createMutator("POST", "/chat/stream");
180
+
181
+ const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {
182
+ return (data ?? [])
183
+ .filter((item) => item.type === "response.output_text.delta")
184
+ .map((item) => item.delta)
185
+ .join("");
186
+ });
187
+
188
+ return {
189
+ useChatStream: chatStream,
190
+ useAggregatedMessage: builder.createStore(aggregatedMessage),
191
+ };
192
+ }
193
+
194
+ const client = createChatClient();
195
+ expect(client.useAggregatedMessage).toBeDefined();
196
+ ```
197
+
198
+ ## Arbitrary Values and Custom Functions
199
+
200
+ Wrap custom objects with `builder.createStore` to make properties reactive. Functions remain
201
+ unchanged.
202
+
203
+ ```typescript @fragno-test:arbitrary-values
204
+ // should wrap custom objects and functions
205
+ export function createChatClient(fragnoConfig: FragnoPublicClientConfig = {}) {
206
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
207
+
208
+ const chatStream = builder.createMutator("POST", "/chat/stream");
209
+
210
+ const aggregatedMessage = computed(chatStream.mutatorStore, ({ data }) => {
211
+ return (data ?? []).map((item) => item.delta).join("");
212
+ });
213
+
214
+ function sendMessage(message: string) {
215
+ chatStream.mutatorStore.mutate({
216
+ body: {
217
+ messages: [{ role: "user", content: message }],
218
+ },
219
+ });
220
+ }
221
+
222
+ return {
223
+ useSendMessage: builder.createStore({
224
+ response: aggregatedMessage,
225
+ responseLoading: computed(chatStream.mutatorStore, ({ loading }) => loading),
226
+ sendMessage,
227
+ }),
228
+ };
229
+ }
230
+
231
+ const client = createChatClient();
232
+ expect(client.useSendMessage).toBeDefined();
233
+ ```
234
+
235
+ Only top-level properties are made reactive. Deeply nested keys are not transformed.
236
+
237
+ ## Custom Fetcher Configuration
238
+
239
+ Set default fetch options or provide a custom fetch function.
240
+
241
+ ```typescript @fragno-test:custom-fetcher-options
242
+ // should set custom fetch options
243
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
244
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes], {
245
+ type: "options",
246
+ options: { credentials: "include" },
247
+ });
248
+
249
+ return {
250
+ useTodos: builder.createHook("/todos"),
251
+ };
252
+ }
253
+
254
+ const client = createTodoClient();
255
+ expect(client.useTodos).toBeDefined();
256
+ ```
257
+
258
+ ```typescript @fragno-test:custom-fetch-function
259
+ // should provide custom fetch function
260
+ const customFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
261
+ return fetch(url, { ...init, headers: { ...init?.headers, "X-Custom": "header" } });
262
+ };
263
+
264
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
265
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes], {
266
+ type: "function",
267
+ fetcher: customFetch,
268
+ });
269
+
270
+ return {
271
+ useTodos: builder.createHook("/todos"),
272
+ };
273
+ }
274
+
275
+ const client = createTodoClient();
276
+ expect(client.useTodos).toBeDefined();
277
+ ```
278
+
279
+ User configuration takes precedence: custom fetch functions override everything, RequestInit options
280
+ deep merge with user values winning conflicts.
281
+
282
+ ## Custom Backend Calls
283
+
284
+ Use `buildUrl()` and `getFetcher()` for requests beyond `createHook`/`createMutator`.
285
+
286
+ ```typescript @fragno-test:custom-backend-calls
287
+ // should build URLs and get fetcher for custom calls
288
+ export function createTodoClient(fragnoConfig: FragnoPublicClientConfig = {}) {
289
+ const builder = createClientBuilder(todoFragment, fragnoConfig, [routes]);
290
+
291
+ async function customCall(userId: string) {
292
+ const { fetcher, defaultOptions } = builder.getFetcher();
293
+ const url = builder.buildUrl("/todos/:id", { path: { id: userId } });
294
+ return fetcher(url, { ...defaultOptions, method: "GET" }).then((r) => r.json());
295
+ }
296
+
297
+ return {
298
+ useTodos: builder.createHook("/todos"),
299
+ customCall,
300
+ };
301
+ }
302
+
303
+ const client = createTodoClient();
304
+ expect(client.customCall).toBeDefined();
305
+ ```
@@ -0,0 +1,88 @@
1
+ # Database Adapters
2
+
3
+ Database adapters connect Fragno's database API to your existing ORM (Kysely or Drizzle). They allow
4
+ fragments to work with your application's database without dictating which ORM you use.
5
+
6
+ ```typescript @fragno-imports
7
+ import type { DatabaseAdapter } from "@fragno-dev/db";
8
+ ```
9
+
10
+ ## What is a Database Adapter?
11
+
12
+ A database adapter is a bridge between Fragno's type-safe database API and your underlying ORM. It
13
+ translates Fragno's query operations into ORM-specific syntax.
14
+
15
+ ```typescript @fragno-test:what-is-adapter types-only
16
+ // Adapters implement the DatabaseAdapter interface
17
+ declare const adapter: DatabaseAdapter;
18
+
19
+ // Fragments receive an adapter through their configuration
20
+ interface FragmentConfig {
21
+ databaseAdapter: DatabaseAdapter;
22
+ }
23
+ ```
24
+
25
+ When a fragment needs database access, users pass an adapter configured with their ORM instance.
26
+
27
+ ## Supported Providers
28
+
29
+ Both KyselyAdapter and DrizzleAdapter support three database providers:
30
+
31
+ - `"postgresql"` - PostgreSQL databases
32
+ - `"mysql"` - MySQL and MariaDB databases
33
+ - `"sqlite"` - SQLite databases
34
+
35
+ Choose the provider that matches your database type when creating an adapter.
36
+
37
+ ## Factory Functions
38
+
39
+ Adapters can be created from factory functions instead of direct ORM instances. This is useful for
40
+ lazy initialization (in serverless environments).
41
+
42
+ ```typescript @fragno-test:factory-functions types-only
43
+ async function createDbConnection() {
44
+ const db = {} as any; // Your ORM instance
45
+ return db;
46
+ }
47
+
48
+ declare const DrizzleAdapter: any;
49
+ export const adapter = new DrizzleAdapter({
50
+ db: createDbConnection,
51
+ provider: "postgresql",
52
+ });
53
+ ```
54
+
55
+ The adapter calls the factory function when it needs a database connection.
56
+
57
+ ## Shared Adapters
58
+
59
+ Multiple fragments can share the same adapter, meaning they all use your application's single
60
+ database connection.
61
+
62
+ ```typescript @fragno-test:shared-adapters types-only
63
+ declare const adapter: DatabaseAdapter;
64
+
65
+ // All fragments use the same adapter
66
+ export interface Fragment1Config {
67
+ databaseAdapter: typeof adapter;
68
+ }
69
+
70
+ export interface Fragment2Config {
71
+ databaseAdapter: typeof adapter;
72
+ }
73
+ ```
74
+
75
+ This ensures fragments integrate seamlessly with your existing database infrastructure.
76
+
77
+ ## Cleanup
78
+
79
+ Adapters manage connection lifecycle automatically. Call `close()` when shutting down your
80
+ application to properly release database connections.
81
+
82
+ ```typescript @fragno-test:cleanup types-only
83
+ declare const adapter: DatabaseAdapter;
84
+
85
+ export async function cleanup() {
86
+ await adapter.close();
87
+ }
88
+ ```
@@ -0,0 +1,316 @@
1
+ # Database Querying
2
+
3
+ Fragno provides a unified database query API that works across different ORMs. This guide covers
4
+ CRUD operations and querying with conditions.
5
+
6
+ ```typescript @fragno-imports
7
+ import { defineFragmentWithDatabase } from "@fragno-dev/db/fragment";
8
+ import { schema, idColumn, column } from "@fragno-dev/db/schema";
9
+ import type { AbstractQuery } from "@fragno-dev/db/query";
10
+ import { createDatabaseFragmentForTest } from "@fragno-dev/test";
11
+ ```
12
+
13
+ ```typescript @fragno-prelude:schema
14
+ // Example schema for testing
15
+ const userSchema = schema((s) => {
16
+ return s
17
+ .addTable("users", (t) => {
18
+ return t
19
+ .addColumn("id", idColumn())
20
+ .addColumn("email", column("string"))
21
+ .addColumn("name", column("string"))
22
+ .addColumn("age", column("integer").nullable())
23
+ .createIndex("idx_email", ["email"], { unique: true });
24
+ })
25
+ .addTable("posts", (t) => {
26
+ return t
27
+ .addColumn("id", idColumn())
28
+ .addColumn("title", column("string"))
29
+ .addColumn("content", column("string"))
30
+ .addColumn("authorId", column("string"))
31
+ .addColumn("publishedAt", column("timestamp").nullable())
32
+ .createIndex("idx_author", ["authorId"]);
33
+ });
34
+ });
35
+
36
+ type UserSchema = typeof userSchema;
37
+ ```
38
+
39
+ ```typescript @fragno-test-init
40
+ // Create a test fragment with database
41
+ const testFragmentDef = defineFragmentWithDatabase("test-db-fragment").withDatabase(userSchema);
42
+
43
+ const { fragment, test } = await createDatabaseFragmentForTest(
44
+ { definition: testFragmentDef, routes: [] },
45
+ {
46
+ adapter: { type: "kysely-sqlite" },
47
+ },
48
+ );
49
+
50
+ const db = fragment.db;
51
+ ```
52
+
53
+ ## Create
54
+
55
+ Create a single record in the database.
56
+
57
+ ```typescript @fragno-test:create-user
58
+ // should create a single user
59
+ const userId = await db.create("users", {
60
+ id: "user-123",
61
+ email: "john@example.com",
62
+ name: "John Doe",
63
+ age: 30,
64
+ });
65
+
66
+ expect(userId).toBeDefined();
67
+ expect(userId.valueOf()).toBe("user-123");
68
+ ```
69
+
70
+ The `create` method returns a `FragnoId` object representing the created record's ID.
71
+
72
+ ## Create Many
73
+
74
+ Create multiple records at once.
75
+
76
+ ```typescript @fragno-test:create-many
77
+ // should create multiple users at once
78
+ const userIds = await db.createMany("users", [
79
+ {
80
+ id: "user-1",
81
+ email: "user1@example.com",
82
+ name: "User One",
83
+ age: 25,
84
+ },
85
+ {
86
+ id: "user-2",
87
+ email: "user2@example.com",
88
+ name: "User Two",
89
+ age: 35,
90
+ },
91
+ ]);
92
+
93
+ expect(userIds).toHaveLength(2);
94
+ expect(userIds[0].valueOf()).toBe("user-1");
95
+ expect(userIds[1].valueOf()).toBe("user-2");
96
+ ```
97
+
98
+ ## Find First
99
+
100
+ Query for a single record using an index.
101
+
102
+ ```typescript @fragno-test:find-user-by-email
103
+ // should find user by email using index
104
+ await db.create("users", {
105
+ id: "user-find-1",
106
+ email: "findme@example.com",
107
+ name: "Find Me",
108
+ age: 28,
109
+ });
110
+
111
+ const user = await db.findFirst("users", (b) =>
112
+ b.whereIndex("idx_email", (eb) => eb("email", "=", "findme@example.com")),
113
+ );
114
+
115
+ expect(user).toBeDefined();
116
+ expect(user?.email).toBe("findme@example.com");
117
+ expect(user?.name).toBe("Find Me");
118
+ ```
119
+
120
+ Use `findFirst` when you expect a single result or want to get the first matching record.
121
+
122
+ ## Find First with Select
123
+
124
+ Query a single record and select specific columns.
125
+
126
+ ```typescript @fragno-test:find-user-select
127
+ export async function findUserEmailOnly(userId: string) {
128
+ const user = await db.findFirst("users", (b) =>
129
+ b.whereIndex("primary", (eb) => eb("id", "=", userId)).select(["id", "email"]),
130
+ );
131
+
132
+ return user; // Returns only id and email fields
133
+ }
134
+ ```
135
+
136
+ ## Find Many
137
+
138
+ Query for multiple records matching conditions.
139
+
140
+ ```typescript @fragno-test:find-many
141
+ // should find multiple posts by author
142
+ await db.createMany("posts", [
143
+ {
144
+ id: "post-1",
145
+ title: "First Post",
146
+ content: "Content 1",
147
+ authorId: "author-123",
148
+ publishedAt: null,
149
+ },
150
+ {
151
+ id: "post-2",
152
+ title: "Second Post",
153
+ content: "Content 2",
154
+ authorId: "author-123",
155
+ publishedAt: null,
156
+ },
157
+ {
158
+ id: "post-3",
159
+ title: "Other Post",
160
+ content: "Content 3",
161
+ authorId: "author-456",
162
+ publishedAt: null,
163
+ },
164
+ ]);
165
+
166
+ const posts = await db.find("posts", (b) =>
167
+ b.whereIndex("idx_author", (eb) => eb("authorId", "=", "author-123")),
168
+ );
169
+
170
+ expect(posts).toHaveLength(2);
171
+ expect(posts[0].authorId).toBe("author-123");
172
+ expect(posts[1].authorId).toBe("author-123");
173
+ ```
174
+
175
+ The `find` method returns all records matching the where clause.
176
+
177
+ ## Find with Pagination
178
+
179
+ Limit the number of results returned.
180
+
181
+ ```typescript @fragno-test:find-paginated
182
+ export async function findUsersPaginated(pageSize: number) {
183
+ const users = await db.find("users", (b) => b.whereIndex("primary").pageSize(pageSize));
184
+
185
+ return users;
186
+ }
187
+ ```
188
+
189
+ ## Cursor-Based Pagination
190
+
191
+ Use `findWithCursor` for efficient pagination with cursor support.
192
+
193
+ ```typescript @fragno-test:cursor-pagination
194
+ const firstPage = await db.findWithCursor("users", (b) =>
195
+ b.whereIndex("idx_email").orderByIndex("idx_email", "asc").pageSize(2),
196
+ );
197
+
198
+ const cursor = firstPage.cursor;
199
+ if (cursor) {
200
+ const nextPage = await db.findWithCursor("users", (b) => b.after(cursor));
201
+ }
202
+ ```
203
+
204
+ The `findWithCursor` method returns a `CursorResult` object containing:
205
+
206
+ - `items`: The array of results for the current page
207
+ - `cursor`: A `Cursor` object for fetching the next page (undefined if no more results)
208
+
209
+ The cursor automatically stores pagination metadata (index, ordering, page size), so you can simply
210
+ pass it to `b.after()` for the next page.
211
+
212
+ ## Find All
213
+
214
+ Query all records from a table.
215
+
216
+ ```typescript @fragno-test:find-all
217
+ export async function findAllUsers() {
218
+ const users = await db.find("users", (b) => b.whereIndex("primary"));
219
+
220
+ return users;
221
+ }
222
+ ```
223
+
224
+ When using `whereIndex("primary")` without conditions, it returns all records.
225
+
226
+ ## Update
227
+
228
+ Update a single record by ID.
229
+
230
+ ```typescript @fragno-test:update-user
231
+ // should update a user's email
232
+ await db.create("users", {
233
+ id: "user-update-1",
234
+ email: "old@example.com",
235
+ name: "Update Test",
236
+ age: 30,
237
+ });
238
+
239
+ await db.update("users", "user-update-1", (b) =>
240
+ b.set({
241
+ email: "new@example.com",
242
+ }),
243
+ );
244
+
245
+ const updatedUser = await db.findFirst("users", (b) =>
246
+ b.whereIndex("primary", (eb) => eb("id", "=", "user-update-1")),
247
+ );
248
+
249
+ expect(updatedUser?.email).toBe("new@example.com");
250
+ expect(updatedUser?.name).toBe("Update Test");
251
+ ```
252
+
253
+ The `update` method modifies a single record identified by its ID.
254
+
255
+ ## Update Many
256
+
257
+ Update multiple records matching a condition.
258
+
259
+ ```typescript @fragno-test:update-many
260
+ export async function updatePostsPublishedDate(authorId: string, publishedAt: Date) {
261
+ await db.updateMany("posts", (b) =>
262
+ b.whereIndex("idx_author", (eb) => eb("authorId", "=", authorId)).set({ publishedAt }),
263
+ );
264
+ }
265
+ ```
266
+
267
+ `updateMany` finds all matching records and updates them.
268
+
269
+ ## Delete
270
+
271
+ Delete a single record by ID.
272
+
273
+ ```typescript @fragno-test:delete-user
274
+ // should delete a user by ID
275
+ await db.create("users", {
276
+ id: "user-delete-1",
277
+ email: "delete@example.com",
278
+ name: "Delete Me",
279
+ age: 25,
280
+ });
281
+
282
+ await db.delete("users", "user-delete-1");
283
+
284
+ const deletedUser = await db.findFirst("users", (b) =>
285
+ b.whereIndex("primary", (eb) => eb("id", "=", "user-delete-1")),
286
+ );
287
+
288
+ expect(deletedUser).toBeNull();
289
+ ```
290
+
291
+ ## Delete Many
292
+
293
+ Delete multiple records matching a condition.
294
+
295
+ ```typescript @fragno-test:delete-many
296
+ export async function deletePostsByAuthor(authorId: string) {
297
+ await db.deleteMany("posts", (b) =>
298
+ b.whereIndex("idx_author", (eb) => eb("authorId", "=", authorId)),
299
+ );
300
+ }
301
+ ```
302
+
303
+ `deleteMany` finds all matching records and deletes them.
304
+
305
+ ## Querying with Conditions
306
+
307
+ Use expression builders within `whereIndex()` to create queries with conditions.
308
+
309
+ ```typescript @fragno-test:query-conditions
310
+ const user = await db.findFirst("users", (b) =>
311
+ b.whereIndex("idx_email", (eb) => eb("email", "=", "adult1@example.com")),
312
+ );
313
+ ```
314
+
315
+ The expression builder (the `eb` parameter) is used within the `whereIndex()` callback to specify
316
+ conditions on indexed columns.