@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.
- package/README.md +4 -2
- 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 +301 -0
- package/dist/subjects/database-adapters.md +88 -0
- package/dist/subjects/database-querying.md +318 -0
- package/dist/subjects/defining-routes.md +229 -0
- package/dist/subjects/drizzle-adapter.md +60 -0
- package/dist/subjects/fragment-instantiation.md +116 -0
- package/dist/subjects/fragment-services.md +189 -0
- package/dist/subjects/kysely-adapter.md +59 -0
- package/package.json +11 -4
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Defining Routes
|
|
2
|
+
|
|
3
|
+
Routes are the core of a Fragno fragment, defining HTTP endpoints that handle requests and return
|
|
4
|
+
responses. This guide covers the essential patterns for defining routes.
|
|
5
|
+
|
|
6
|
+
```typescript @fragno-imports
|
|
7
|
+
import {
|
|
8
|
+
defineFragment,
|
|
9
|
+
defineRoute,
|
|
10
|
+
defineRoutes,
|
|
11
|
+
instantiate,
|
|
12
|
+
type FragnoPublicConfig,
|
|
13
|
+
} from "@fragno-dev/core";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic GET Route
|
|
18
|
+
|
|
19
|
+
A simple GET route with an output schema.
|
|
20
|
+
|
|
21
|
+
```typescript @fragno-test:basic-get-route
|
|
22
|
+
// should define a basic GET route
|
|
23
|
+
const basicGetRoute = defineRoute({
|
|
24
|
+
method: "GET",
|
|
25
|
+
path: "/hello",
|
|
26
|
+
outputSchema: z.string(),
|
|
27
|
+
handler: async (_, { json }) => {
|
|
28
|
+
return json("Hello, World!");
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(basicGetRoute.method).toBe("GET");
|
|
33
|
+
expect(basicGetRoute.path).toBe("/hello");
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `outputSchema` uses Zod (or any Standard Schema compatible library) to define the response type.
|
|
37
|
+
The handler receives a context object and helpers like `json()` for sending responses.
|
|
38
|
+
|
|
39
|
+
## POST Route with Input Schema
|
|
40
|
+
|
|
41
|
+
Routes can accept and validate request bodies using `inputSchema`.
|
|
42
|
+
|
|
43
|
+
```typescript @fragno-test:post-route
|
|
44
|
+
// should define a POST route with input schema
|
|
45
|
+
const createItemRoute = defineRoute({
|
|
46
|
+
method: "POST",
|
|
47
|
+
path: "/items",
|
|
48
|
+
inputSchema: z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
}),
|
|
52
|
+
outputSchema: z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
name: z.string(),
|
|
55
|
+
description: z.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
handler: async ({ input }, { json }) => {
|
|
58
|
+
const data = await input.valid();
|
|
59
|
+
|
|
60
|
+
return json({
|
|
61
|
+
id: "item-123",
|
|
62
|
+
name: data.name,
|
|
63
|
+
description: data.description,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(createItemRoute.method).toBe("POST");
|
|
69
|
+
expect(createItemRoute.inputSchema).toBeDefined();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `input.valid()` method validates the request body against the schema and returns the typed data.
|
|
73
|
+
|
|
74
|
+
## Query Parameters
|
|
75
|
+
|
|
76
|
+
Routes can declare query parameters they expect to receive.
|
|
77
|
+
|
|
78
|
+
```typescript @fragno-test:query-parameters
|
|
79
|
+
// should define a route with query parameters
|
|
80
|
+
const listItemsRoute = defineRoute({
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: "/items",
|
|
83
|
+
queryParameters: ["page", "limit", "filter"],
|
|
84
|
+
outputSchema: z.array(
|
|
85
|
+
z.object({
|
|
86
|
+
id: z.string(),
|
|
87
|
+
name: z.string(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
handler: async ({ query }, { json }) => {
|
|
91
|
+
const page = query.get("page") || "1";
|
|
92
|
+
const limit = query.get("limit") || "10";
|
|
93
|
+
const filter = query.get("filter");
|
|
94
|
+
|
|
95
|
+
return json([
|
|
96
|
+
{ id: "1", name: "Item 1" },
|
|
97
|
+
{ id: "2", name: "Item 2" },
|
|
98
|
+
]);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(listItemsRoute.queryParameters).toEqual(["page", "limit", "filter"]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Query parameters are accessed via `query.get(name)` from the context.
|
|
106
|
+
|
|
107
|
+
## Error Handling
|
|
108
|
+
|
|
109
|
+
Routes can define custom error codes and return errors with appropriate status codes.
|
|
110
|
+
|
|
111
|
+
```typescript @fragno-test:error-handling
|
|
112
|
+
// should define a route with error codes
|
|
113
|
+
const validateItemRoute = defineRoute({
|
|
114
|
+
method: "POST",
|
|
115
|
+
path: "/validate",
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
value: z.string(),
|
|
118
|
+
}),
|
|
119
|
+
outputSchema: z.object({ valid: z.boolean() }),
|
|
120
|
+
errorCodes: ["INVALID_VALUE", "VALUE_TOO_SHORT"],
|
|
121
|
+
handler: async ({ input }, { json, error }) => {
|
|
122
|
+
const data = await input.valid();
|
|
123
|
+
|
|
124
|
+
if (data.value.length < 3) {
|
|
125
|
+
return error(
|
|
126
|
+
{
|
|
127
|
+
message: "Value must be at least 3 characters",
|
|
128
|
+
code: "VALUE_TOO_SHORT",
|
|
129
|
+
},
|
|
130
|
+
400,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!/^[a-z]+$/.test(data.value)) {
|
|
135
|
+
return error(
|
|
136
|
+
{
|
|
137
|
+
message: "Value must contain only lowercase letters",
|
|
138
|
+
code: "INVALID_VALUE",
|
|
139
|
+
},
|
|
140
|
+
400,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return json({ valid: true });
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(validateItemRoute.errorCodes).toEqual(["INVALID_VALUE", "VALUE_TOO_SHORT"]);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The `error()` helper sends an error response with a custom error code and HTTP status.
|
|
152
|
+
|
|
153
|
+
## Using Dependencies
|
|
154
|
+
|
|
155
|
+
Routes can access dependencies defined in `withDependencies` through route factories.
|
|
156
|
+
|
|
157
|
+
```typescript @fragno-test:using-dependencies
|
|
158
|
+
interface AppConfig {
|
|
159
|
+
apiKey?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const definition = defineFragment<AppConfig>("test")
|
|
163
|
+
.withDependencies(({ config }) => ({ timestamp: Date.now() }))
|
|
164
|
+
.build();
|
|
165
|
+
|
|
166
|
+
export const routesWithDeps = defineRoutes<typeof definition>().create(({ config, deps }) => {
|
|
167
|
+
return [
|
|
168
|
+
defineRoute({
|
|
169
|
+
method: "GET",
|
|
170
|
+
path: "/config-info",
|
|
171
|
+
outputSchema: z.object({
|
|
172
|
+
hasApiKey: z.boolean(),
|
|
173
|
+
timestamp: z.number(),
|
|
174
|
+
}),
|
|
175
|
+
handler: async (_, { json }) => {
|
|
176
|
+
return json({
|
|
177
|
+
hasApiKey: !!config.apiKey,
|
|
178
|
+
timestamp: deps.timestamp,
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Dependencies are passed to the route factory function and can be used in route handlers.
|
|
187
|
+
|
|
188
|
+
## Using Services
|
|
189
|
+
|
|
190
|
+
Services defined in `providesService` can be used in routes for business logic.
|
|
191
|
+
|
|
192
|
+
```typescript @fragno-test:using-services
|
|
193
|
+
const definition = defineFragment("test")
|
|
194
|
+
.withDependencies(({ config }) => ({ timestamp: Date.now() }))
|
|
195
|
+
.providesBaseService(({ defineService }) =>
|
|
196
|
+
defineService({
|
|
197
|
+
getData: () => "Hello, World!",
|
|
198
|
+
processData: async (input: string) => input,
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
.build();
|
|
202
|
+
|
|
203
|
+
export const routesWithServices = defineRoutes<typeof definition>().create(({ services }) => {
|
|
204
|
+
return [
|
|
205
|
+
defineRoute({
|
|
206
|
+
method: "GET",
|
|
207
|
+
path: "/data",
|
|
208
|
+
outputSchema: z.string(),
|
|
209
|
+
handler: async (_, { json }) => {
|
|
210
|
+
const data = services.getData();
|
|
211
|
+
return json(data);
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
defineRoute({
|
|
215
|
+
method: "POST",
|
|
216
|
+
path: "/process",
|
|
217
|
+
inputSchema: z.object({ input: z.string() }),
|
|
218
|
+
outputSchema: z.string(),
|
|
219
|
+
handler: async ({ input }, { json }) => {
|
|
220
|
+
const { input: inputData } = await input.valid();
|
|
221
|
+
const result = await services.processData(inputData);
|
|
222
|
+
return json(result);
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
];
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Services provide reusable business logic that can be shared across multiple routes.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Drizzle Adapter
|
|
2
|
+
|
|
3
|
+
The DrizzleAdapter connects Fragno's database API to your Drizzle ORM instance.
|
|
4
|
+
|
|
5
|
+
```typescript @fragno-imports
|
|
6
|
+
import { DrizzleAdapter } from "@fragno-dev/db/adapters/drizzle";
|
|
7
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
8
|
+
import type { PgliteDatabase } from "drizzle-orm/pglite";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Setup
|
|
12
|
+
|
|
13
|
+
Create a DrizzleAdapter with your Drizzle database instance and provider.
|
|
14
|
+
|
|
15
|
+
```typescript @fragno-test:basic-setup types-only
|
|
16
|
+
interface MyDatabase extends Record<string, unknown> {
|
|
17
|
+
users: {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
posts: {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
content: string;
|
|
26
|
+
authorId: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare const db: NodePgDatabase<MyDatabase>;
|
|
31
|
+
|
|
32
|
+
export const adapter = new DrizzleAdapter({
|
|
33
|
+
db,
|
|
34
|
+
provider: "postgresql",
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The adapter requires your Drizzle instance and the database provider (`"postgresql"`, `"mysql"`, or
|
|
39
|
+
`"sqlite"`).
|
|
40
|
+
|
|
41
|
+
## Factory Function
|
|
42
|
+
|
|
43
|
+
For async or sync database initialization, pass a factory function instead of a direct instance.
|
|
44
|
+
|
|
45
|
+
```typescript @fragno-test:factory-function types-only
|
|
46
|
+
import type { PgliteDatabase } from "drizzle-orm/pglite";
|
|
47
|
+
|
|
48
|
+
async function createDatabase(): Promise<PgliteDatabase> {
|
|
49
|
+
// Async initialization logic
|
|
50
|
+
const db = {} as PgliteDatabase;
|
|
51
|
+
return db;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const adapter = new DrizzleAdapter({
|
|
55
|
+
db: createDatabase,
|
|
56
|
+
provider: "postgresql",
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Factory functions can also be synchronous for lazy initialization scenarios.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Fragment Instantiation
|
|
2
|
+
|
|
3
|
+
Fragno provides the `instantiate` function, which uses a builder pattern to help you and your user
|
|
4
|
+
create Fragments.
|
|
5
|
+
|
|
6
|
+
```typescript @fragno-imports
|
|
7
|
+
import {
|
|
8
|
+
defineFragment,
|
|
9
|
+
defineRoute,
|
|
10
|
+
instantiate,
|
|
11
|
+
type FragnoPublicConfig,
|
|
12
|
+
} from "@fragno-dev/core";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
```typescript @fragno-test:builder-basic
|
|
19
|
+
/** Fragment specific config */
|
|
20
|
+
interface AppConfig {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fragmentDefinition = defineFragment<AppConfig>("api-fragment").build();
|
|
25
|
+
|
|
26
|
+
// User-facing fragment creator function. This is the main integration point for users of your fragment.
|
|
27
|
+
export function createMyFragment(config: AppConfig, options: FragnoPublicConfig = {}) {
|
|
28
|
+
return (
|
|
29
|
+
instantiate(fragmentDefinition)
|
|
30
|
+
.withConfig(config)
|
|
31
|
+
/** Options are passed to Fragno internally */
|
|
32
|
+
.withOptions(options)
|
|
33
|
+
.build()
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// What your user will call to instantiate the fragment:
|
|
38
|
+
const instance = createMyFragment({ apiKey: "my-secret-key" }, { mountRoute: "/api/v1" });
|
|
39
|
+
|
|
40
|
+
expect(instance.name).toBe("api-fragment");
|
|
41
|
+
expect(instance.mountRoute).toBe("/api/v1");
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `withConfig` to provide the fragment's configuration and `withOptions` to set options like
|
|
45
|
+
`mountRoute`.
|
|
46
|
+
|
|
47
|
+
### Builder with Routes
|
|
48
|
+
|
|
49
|
+
Also see the `defining-routes` subject.
|
|
50
|
+
|
|
51
|
+
```typescript @fragno-test:builder-with-routes
|
|
52
|
+
// should add routes using withRoutes
|
|
53
|
+
const fragmentDefinition = defineFragment("routes-fragment").build();
|
|
54
|
+
|
|
55
|
+
const route1 = defineRoute({
|
|
56
|
+
method: "GET",
|
|
57
|
+
path: "/hello",
|
|
58
|
+
outputSchema: z.string(),
|
|
59
|
+
handler: async (_, { json }) => json("Hello"),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const route2 = defineRoute({
|
|
63
|
+
method: "GET",
|
|
64
|
+
path: "/goodbye",
|
|
65
|
+
outputSchema: z.string(),
|
|
66
|
+
handler: async (_, { json }) => json("Goodbye"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const instance = instantiate(fragmentDefinition)
|
|
70
|
+
.withRoutes([route1, route2])
|
|
71
|
+
.withOptions({})
|
|
72
|
+
.build();
|
|
73
|
+
|
|
74
|
+
expect(instance.routes).toHaveLength(2);
|
|
75
|
+
expect(instance.routes[0].path).toBe("/hello");
|
|
76
|
+
expect(instance.routes[1].path).toBe("/goodbye");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Routes can be added as an array using `withRoutes`.
|
|
80
|
+
|
|
81
|
+
### Builder with Services and Dependencies
|
|
82
|
+
|
|
83
|
+
Also see the `fragment-services` subject.
|
|
84
|
+
|
|
85
|
+
```typescript @fragno-test:builder-with-services
|
|
86
|
+
// should provide services and dependencies
|
|
87
|
+
interface AppConfig {
|
|
88
|
+
apiKey: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ILogger {
|
|
92
|
+
log(message: string): void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fragment = defineFragment<AppConfig>("service-fragment")
|
|
96
|
+
.withDependencies(({ config }) => ({
|
|
97
|
+
client: { key: config.apiKey },
|
|
98
|
+
}))
|
|
99
|
+
.usesService<"logger", ILogger>("logger")
|
|
100
|
+
.build();
|
|
101
|
+
|
|
102
|
+
const loggerImpl: ILogger = {
|
|
103
|
+
log: (msg) => console.log(msg),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const instance = instantiate(fragment)
|
|
107
|
+
.withConfig({ apiKey: "my-key" })
|
|
108
|
+
.withServices({ logger: loggerImpl })
|
|
109
|
+
.withOptions({})
|
|
110
|
+
.build();
|
|
111
|
+
|
|
112
|
+
expect(instance.$internal.deps.client.key).toBe("my-key");
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use `withDependencies` to create dependencies from config, and `withServices` to provide required
|
|
116
|
+
services.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Fragment Services
|
|
2
|
+
|
|
3
|
+
Services in Fragno provide a way to define reusable business logic and expose functionality to users
|
|
4
|
+
of your Fragment. Services are also used to share logic between several Fragments.
|
|
5
|
+
|
|
6
|
+
Note that if the goal is to make your user provide certain functionality, it usually makes more
|
|
7
|
+
sense to add a required property to the fragment's config.
|
|
8
|
+
|
|
9
|
+
```typescript @fragno-imports
|
|
10
|
+
import { defineFragment, instantiate, type FragnoPublicConfig } from "@fragno-dev/core";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Providing Services
|
|
14
|
+
|
|
15
|
+
Fragments provide services using `providesService()`. Services can be passed as a direct object or
|
|
16
|
+
via a factory function that receives context (`config`, `deps`, `fragnoConfig`, `defineService`).
|
|
17
|
+
|
|
18
|
+
```typescript @fragno-test:provide-direct-object
|
|
19
|
+
// Object syntax is the simplest way to provide base services.
|
|
20
|
+
const fragmentDefinition = defineFragment("email-fragment")
|
|
21
|
+
.providesBaseService(() => ({
|
|
22
|
+
sendEmail: async (to: string, subject: string) => {
|
|
23
|
+
// Email sending logic here
|
|
24
|
+
},
|
|
25
|
+
}))
|
|
26
|
+
.build();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```typescript @fragno-test:provide-factory-function
|
|
30
|
+
// The factory function receives the fragment's `config`, and `deps` (dependencies).
|
|
31
|
+
const fragmentDefinition = defineFragment("api-fragment")
|
|
32
|
+
.providesBaseService(({ config }) => ({
|
|
33
|
+
makeRequest: async (endpoint: string) => {
|
|
34
|
+
return { data: "response" };
|
|
35
|
+
},
|
|
36
|
+
}))
|
|
37
|
+
.build();
|
|
38
|
+
|
|
39
|
+
const instance = instantiate(fragmentDefinition).withOptions({}).build();
|
|
40
|
+
expect(instance.services.makeRequest).toBeInstanceOf(Function);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Named vs Unnamed Services
|
|
44
|
+
|
|
45
|
+
Unnamed services (shown above) are exposed directly on `instance.services` for direct user
|
|
46
|
+
consumption. Named services provide better organization and enable fragments to work together via
|
|
47
|
+
required services.
|
|
48
|
+
|
|
49
|
+
```typescript @fragno-test:provide-named-services
|
|
50
|
+
// should provide named services
|
|
51
|
+
const fragmentDefinition = defineFragment("logger-fragment")
|
|
52
|
+
.providesService("logger", () => ({
|
|
53
|
+
log: (message: string) => console.log(message),
|
|
54
|
+
error: (message: string) => console.error(message),
|
|
55
|
+
}))
|
|
56
|
+
.build();
|
|
57
|
+
|
|
58
|
+
const instance = instantiate(fragmentDefinition).withOptions({}).build();
|
|
59
|
+
expect(instance.services.logger.log).toBeDefined();
|
|
60
|
+
expect(instance.services.logger.error).toBeDefined();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Using `defineService` helper method
|
|
64
|
+
|
|
65
|
+
Use `defineService` when service methods need proper `this` binding. This is particularly important
|
|
66
|
+
for database fragments where methods need access to the current unit of work.
|
|
67
|
+
|
|
68
|
+
```typescript @fragno-test:chaining-services
|
|
69
|
+
// should chain multiple service definitions
|
|
70
|
+
const fragmentDefinition = defineFragment("multi-service-fragment")
|
|
71
|
+
.providesService("logger", ({ defineService }) =>
|
|
72
|
+
defineService({
|
|
73
|
+
log: (msg: string) => console.log(msg),
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
.providesService("validator", ({ defineService }) =>
|
|
77
|
+
defineService({
|
|
78
|
+
validate: (input: string) => input.length > 0,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
.build();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Using Services
|
|
85
|
+
|
|
86
|
+
Fragments specify required services using `usesService`. Services can be marked as optional using
|
|
87
|
+
`usesOptionalService`, making them `undefined` if not provided.
|
|
88
|
+
|
|
89
|
+
```typescript @fragno-test:declaring-required-service
|
|
90
|
+
// should require a service from the user
|
|
91
|
+
interface IEmailService {
|
|
92
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fragmentDefinition = defineFragment("notification-fragment")
|
|
96
|
+
.usesService<"email", IEmailService>("email")
|
|
97
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
98
|
+
sendNotification: (to: string, subject: string, body: string) =>
|
|
99
|
+
serviceDeps.email.sendEmail(to, subject, body),
|
|
100
|
+
}))
|
|
101
|
+
.build();
|
|
102
|
+
|
|
103
|
+
const emailImpl: IEmailService = {
|
|
104
|
+
sendEmail: async (to, subject, body) => {
|
|
105
|
+
// Implementation
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const instance = instantiate(fragmentDefinition)
|
|
110
|
+
.withServices({ email: emailImpl })
|
|
111
|
+
.withOptions({})
|
|
112
|
+
.build();
|
|
113
|
+
|
|
114
|
+
expect(instance.services.sendNotification).toBeDefined();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Optional Services
|
|
118
|
+
|
|
119
|
+
```typescript @fragno-test:optional-service
|
|
120
|
+
// should mark a service as optional
|
|
121
|
+
interface ILogger {
|
|
122
|
+
log(message: string): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fragmentDefinition = defineFragment("app-fragment")
|
|
126
|
+
.usesOptionalService<"logger", ILogger>("logger")
|
|
127
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
128
|
+
maybeLog: (message: string) => {
|
|
129
|
+
if (serviceDeps.logger) {
|
|
130
|
+
serviceDeps.logger.log(message);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}))
|
|
134
|
+
.build();
|
|
135
|
+
|
|
136
|
+
// Can instantiate without providing the service
|
|
137
|
+
const instance = instantiate(fragmentDefinition).withOptions({}).build();
|
|
138
|
+
|
|
139
|
+
expect(instance).toBeDefined();
|
|
140
|
+
expect(instance.services.maybeLog).toBeDefined();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Using Services in Provided Services
|
|
144
|
+
|
|
145
|
+
Used services are available via `serviceDeps` in the factory function context, enabling composition.
|
|
146
|
+
|
|
147
|
+
```typescript @fragno-test:using-in-provided
|
|
148
|
+
// should use external services in provided services
|
|
149
|
+
interface IEmailService {
|
|
150
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fragmentDefinition = defineFragment("welcome-fragment")
|
|
154
|
+
.usesService<"email", IEmailService>("email")
|
|
155
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
156
|
+
sendWelcomeEmail: async (to: string) => {
|
|
157
|
+
await serviceDeps.email.sendEmail(to, "Welcome!", "Welcome to our service!");
|
|
158
|
+
},
|
|
159
|
+
}))
|
|
160
|
+
.build();
|
|
161
|
+
|
|
162
|
+
const emailImpl: IEmailService = {
|
|
163
|
+
sendEmail: async () => {},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const instance = instantiate(fragmentDefinition)
|
|
167
|
+
.withServices({ email: emailImpl })
|
|
168
|
+
.withOptions({})
|
|
169
|
+
.build();
|
|
170
|
+
|
|
171
|
+
expect(instance.services.sendWelcomeEmail).toBeDefined();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Missing Required Services
|
|
175
|
+
|
|
176
|
+
```typescript @fragno-test:missing-required-service
|
|
177
|
+
// should throw when required service not provided
|
|
178
|
+
interface IStorageService {
|
|
179
|
+
save(key: string, value: string): Promise<void>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const fragmentDefinition = defineFragment("storage-fragment")
|
|
183
|
+
.usesService<"storage", IStorageService>("storage")
|
|
184
|
+
.build();
|
|
185
|
+
|
|
186
|
+
expect(() => {
|
|
187
|
+
instantiate(fragmentDefinition).withOptions({}).build();
|
|
188
|
+
}).toThrow("Fragment 'storage-fragment' requires service 'storage' but it was not provided");
|
|
189
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Kysely Adapter
|
|
2
|
+
|
|
3
|
+
The KyselyAdapter connects Fragno's database API to your Kysely database instance.
|
|
4
|
+
|
|
5
|
+
```typescript @fragno-imports
|
|
6
|
+
import { KyselyAdapter } from "@fragno-dev/db/adapters/kysely";
|
|
7
|
+
import { Kysely } from "kysely";
|
|
8
|
+
import type { Dialect } from "kysely";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Setup
|
|
12
|
+
|
|
13
|
+
Create a KyselyAdapter with your Kysely database instance and provider.
|
|
14
|
+
|
|
15
|
+
```typescript @fragno-test:basic-setup types-only
|
|
16
|
+
interface MyDatabase {
|
|
17
|
+
users: {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
posts: {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
content: string;
|
|
26
|
+
authorId: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare const dialect: Dialect;
|
|
31
|
+
|
|
32
|
+
export const db = new Kysely<MyDatabase>({
|
|
33
|
+
dialect,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const adapter = new KyselyAdapter({
|
|
37
|
+
db,
|
|
38
|
+
provider: "postgresql",
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The adapter requires your Kysely instance and the database provider (`"postgresql"`, `"mysql"`, or
|
|
43
|
+
`"sqlite"`).
|
|
44
|
+
|
|
45
|
+
## Factory Function
|
|
46
|
+
|
|
47
|
+
For async database initialization, pass a factory function instead of a direct instance.
|
|
48
|
+
|
|
49
|
+
```typescript @fragno-test:factory-function types-only
|
|
50
|
+
async function createDatabase() {
|
|
51
|
+
// Async initialization logic
|
|
52
|
+
return new Kysely({ dialect: {} as Dialect });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const adapter = new KyselyAdapter({
|
|
56
|
+
db: createDatabase,
|
|
57
|
+
provider: "postgresql",
|
|
58
|
+
});
|
|
59
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragno-dev/corpus",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -21,12 +21,19 @@
|
|
|
21
21
|
"drizzle-orm": "^0.44.7",
|
|
22
22
|
"kysely": "^0.28.0",
|
|
23
23
|
"zod": "^4.0.5",
|
|
24
|
-
"@fragno-dev/core": "0.1.
|
|
25
|
-
"@fragno-dev/db": "0.1.
|
|
26
|
-
"@fragno-dev/test": "0.1.
|
|
24
|
+
"@fragno-dev/core": "0.1.9",
|
|
25
|
+
"@fragno-dev/db": "0.1.15",
|
|
26
|
+
"@fragno-dev/test": "0.1.13",
|
|
27
27
|
"@fragno-private/typescript-config": "0.0.1",
|
|
28
28
|
"@fragno-private/vitest-config": "0.0.0"
|
|
29
29
|
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/rejot-dev/fragno.git",
|
|
33
|
+
"directory": "packages/corpus"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://fragno.dev",
|
|
36
|
+
"license": "MIT",
|
|
30
37
|
"scripts": {
|
|
31
38
|
"build": "tsdown",
|
|
32
39
|
"build:watch": "tsdown --watch",
|