@fragno-dev/corpus 0.0.4 → 0.0.6

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