@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.
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141 -93
- package/dist/index.js.map +1 -1
- package/dist/subjects/client-state-management.md +305 -0
- package/dist/subjects/database-adapters.md +88 -0
- package/dist/subjects/database-querying.md +316 -0
- package/dist/subjects/defining-routes.md +272 -0
- package/dist/subjects/drizzle-adapter.md +60 -0
- package/dist/subjects/fragment-instantiation.md +113 -0
- package/dist/subjects/fragment-services.md +178 -0
- package/dist/subjects/kysely-adapter.md +59 -0
- package/package.json +8 -1
|
@@ -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.
|