@fragno-dev/corpus 0.0.3 → 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,272 @@
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 { defineRoute, defineRoutes, defineFragment, createFragment } from "@fragno-dev/core";
8
+ import type { FragnoPublicConfig } from "@fragno-dev/core";
9
+ import { z } from "zod";
10
+ ```
11
+
12
+ ## Basic GET Route
13
+
14
+ A simple GET route with an output schema.
15
+
16
+ ```typescript @fragno-test:basic-get-route
17
+ // should define a basic GET route
18
+ const basicGetRoute = defineRoute({
19
+ method: "GET",
20
+ path: "/hello",
21
+ outputSchema: z.string(),
22
+ handler: async (_, { json }) => {
23
+ return json("Hello, World!");
24
+ },
25
+ });
26
+
27
+ expect(basicGetRoute.method).toBe("GET");
28
+ expect(basicGetRoute.path).toBe("/hello");
29
+ ```
30
+
31
+ The `outputSchema` uses Zod (or any Standard Schema compatible library) to define the response type.
32
+ The handler receives a context object and helpers like `json()` for sending responses.
33
+
34
+ ## POST Route with Input Schema
35
+
36
+ Routes can accept and validate request bodies using `inputSchema`.
37
+
38
+ ```typescript @fragno-test:post-route
39
+ // should define a POST route with input schema
40
+ const createItemRoute = defineRoute({
41
+ method: "POST",
42
+ path: "/items",
43
+ inputSchema: z.object({
44
+ name: z.string(),
45
+ description: z.string().optional(),
46
+ }),
47
+ outputSchema: z.object({
48
+ id: z.string(),
49
+ name: z.string(),
50
+ description: z.string().optional(),
51
+ }),
52
+ handler: async ({ input }, { json }) => {
53
+ const data = await input.valid();
54
+
55
+ return json({
56
+ id: "item-123",
57
+ name: data.name,
58
+ description: data.description,
59
+ });
60
+ },
61
+ });
62
+
63
+ expect(createItemRoute.method).toBe("POST");
64
+ expect(createItemRoute.inputSchema).toBeDefined();
65
+ ```
66
+
67
+ The `input.valid()` method validates the request body against the schema and returns the typed data.
68
+
69
+ ## Query Parameters
70
+
71
+ Routes can declare query parameters they expect to receive.
72
+
73
+ ```typescript @fragno-test:query-parameters
74
+ // should define a route with query parameters
75
+ const listItemsRoute = defineRoute({
76
+ method: "GET",
77
+ path: "/items",
78
+ queryParameters: ["page", "limit", "filter"],
79
+ outputSchema: z.array(
80
+ z.object({
81
+ id: z.string(),
82
+ name: z.string(),
83
+ }),
84
+ ),
85
+ handler: async ({ query }, { json }) => {
86
+ const page = query.get("page") || "1";
87
+ const limit = query.get("limit") || "10";
88
+ const filter = query.get("filter");
89
+
90
+ return json([
91
+ { id: "1", name: "Item 1" },
92
+ { id: "2", name: "Item 2" },
93
+ ]);
94
+ },
95
+ });
96
+
97
+ expect(listItemsRoute.queryParameters).toEqual(["page", "limit", "filter"]);
98
+ ```
99
+
100
+ Query parameters are accessed via `query.get(name)` from the context.
101
+
102
+ ## Error Handling
103
+
104
+ Routes can define custom error codes and return errors with appropriate status codes.
105
+
106
+ ```typescript @fragno-test:error-handling
107
+ // should define a route with error codes
108
+ const validateItemRoute = defineRoute({
109
+ method: "POST",
110
+ path: "/validate",
111
+ inputSchema: z.object({
112
+ value: z.string(),
113
+ }),
114
+ outputSchema: z.object({ valid: z.boolean() }),
115
+ errorCodes: ["INVALID_VALUE", "VALUE_TOO_SHORT"],
116
+ handler: async ({ input }, { json, error }) => {
117
+ const data = await input.valid();
118
+
119
+ if (data.value.length < 3) {
120
+ return error(
121
+ {
122
+ message: "Value must be at least 3 characters",
123
+ code: "VALUE_TOO_SHORT",
124
+ },
125
+ 400,
126
+ );
127
+ }
128
+
129
+ if (!/^[a-z]+$/.test(data.value)) {
130
+ return error(
131
+ {
132
+ message: "Value must contain only lowercase letters",
133
+ code: "INVALID_VALUE",
134
+ },
135
+ 400,
136
+ );
137
+ }
138
+
139
+ return json({ valid: true });
140
+ },
141
+ });
142
+
143
+ expect(validateItemRoute.errorCodes).toEqual(["INVALID_VALUE", "VALUE_TOO_SHORT"]);
144
+ ```
145
+
146
+ The `error()` helper sends an error response with a custom error code and HTTP status.
147
+
148
+ ## Using Dependencies
149
+
150
+ Routes can access dependencies defined in `withDependencies` through route factories.
151
+
152
+ ```typescript @fragno-test:using-dependencies
153
+ interface AppConfig {
154
+ apiKey: string;
155
+ }
156
+
157
+ interface AppDeps {
158
+ config: AppConfig;
159
+ timestamp: number;
160
+ }
161
+
162
+ export const routesWithDeps = defineRoutes<AppConfig, AppDeps>().create(({ deps }) => {
163
+ return [
164
+ defineRoute({
165
+ method: "GET",
166
+ path: "/config-info",
167
+ outputSchema: z.object({
168
+ hasApiKey: z.boolean(),
169
+ timestamp: z.number(),
170
+ }),
171
+ handler: async (_, { json }) => {
172
+ return json({
173
+ hasApiKey: !!deps.config.apiKey,
174
+ timestamp: deps.timestamp,
175
+ });
176
+ },
177
+ }),
178
+ ];
179
+ });
180
+ ```
181
+
182
+ Dependencies are passed to the route factory function and can be used in route handlers.
183
+
184
+ ## Using Services
185
+
186
+ Services defined in `providesService` can be used in routes for business logic.
187
+
188
+ ```typescript @fragno-test:using-services
189
+ interface DataService {
190
+ getData: () => string;
191
+ processData: (input: string) => Promise<string>;
192
+ }
193
+
194
+ export const routesWithServices = defineRoutes<{}, {}, DataService>().create(({ services }) => {
195
+ return [
196
+ defineRoute({
197
+ method: "GET",
198
+ path: "/data",
199
+ outputSchema: z.string(),
200
+ handler: async (_, { json }) => {
201
+ const data = services.getData();
202
+ return json(data);
203
+ },
204
+ }),
205
+ defineRoute({
206
+ method: "POST",
207
+ path: "/process",
208
+ inputSchema: z.object({ input: z.string() }),
209
+ outputSchema: z.string(),
210
+ handler: async ({ input }, { json }) => {
211
+ const { input: inputData } = await input.valid();
212
+ const result = await services.processData(inputData);
213
+ return json(result);
214
+ },
215
+ }),
216
+ ];
217
+ });
218
+ ```
219
+
220
+ Services provide reusable business logic that can be shared across multiple routes.
221
+
222
+ ## Complete Fragment Example
223
+
224
+ A complete example showing how routes integrate with fragment definition.
225
+
226
+ ```typescript @fragno-test:complete-fragment
227
+ interface MyFragmentConfig {
228
+ apiKey: string;
229
+ }
230
+
231
+ interface MyFragmentDeps {
232
+ config: MyFragmentConfig;
233
+ }
234
+
235
+ interface MyFragmentServices {
236
+ getStatus: () => string;
237
+ }
238
+
239
+ const myFragmentDefinition = defineFragment<MyFragmentConfig>("my-fragment")
240
+ .withDependencies(({ config }) => {
241
+ return {
242
+ config,
243
+ };
244
+ })
245
+ .providesService(({ deps, defineService }) => {
246
+ return defineService({
247
+ getStatus: () => `API Key: ${deps.config.apiKey.substring(0, 3)}...`,
248
+ });
249
+ });
250
+
251
+ const myRoutes = defineRoutes<MyFragmentConfig, MyFragmentDeps, MyFragmentServices>().create(
252
+ ({ services }) => {
253
+ return [
254
+ defineRoute({
255
+ method: "GET",
256
+ path: "/status",
257
+ outputSchema: z.string(),
258
+ handler: async (_, { json }) => {
259
+ return json(services.getStatus());
260
+ },
261
+ }),
262
+ ];
263
+ },
264
+ );
265
+
266
+ export function createMyFragment(config: MyFragmentConfig, options: FragnoPublicConfig = {}) {
267
+ return createFragment(myFragmentDefinition, config, [myRoutes], options);
268
+ }
269
+ ```
270
+
271
+ This example shows the complete flow: fragment definition with dependencies and services, route
272
+ factory using those services, and the fragment creation function.
@@ -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,113 @@
1
+ # Fragment Instantiation
2
+
3
+ Fragno provides the `instantiateFragment` function, which uses a builder pattern to help you and
4
+ your user create Fragments.
5
+
6
+ ```typescript @fragno-imports
7
+ import {
8
+ defineFragment,
9
+ createFragment,
10
+ instantiateFragment,
11
+ defineRoute,
12
+ type FragnoPublicConfig,
13
+ } from "@fragno-dev/core";
14
+ import { z } from "zod";
15
+ ```
16
+
17
+ ## Basic Usage
18
+
19
+ ```typescript @fragno-test:builder-basic
20
+ /** Fragment specific config */
21
+ interface AppConfig {
22
+ apiKey: string;
23
+ }
24
+
25
+ const fragment = defineFragment<AppConfig>("api-fragment");
26
+
27
+ // User-facing fragment creator function. This is the main integration point for users of your fragment.
28
+ export function createMyFragment(config: AppConfig, options: FragnoPublicConfig = {}) {
29
+ return (
30
+ instantiateFragment(fragment)
31
+ .withConfig(config)
32
+ /** Options are passed to Fragno internally */
33
+ .withOptions(options)
34
+ .build()
35
+ );
36
+ }
37
+
38
+ // What your user will call to instantiate the fragment:
39
+ const instance = createMyFragment({ apiKey: "my-secret-key" }, { mountRoute: "/api/v1" });
40
+
41
+ expect(instance.config.name).toBe("api-fragment");
42
+ expect(instance.mountRoute).toBe("/api/v1");
43
+ ```
44
+
45
+ Use `withConfig` to provide the fragment's configuration and `withOptions` to set options like
46
+ `mountRoute`.
47
+
48
+ ### Builder with Routes
49
+
50
+ Also see the `defining-routes` subject.
51
+
52
+ ```typescript @fragno-test:builder-with-routes
53
+ // should add routes using withRoutes
54
+ const fragment = defineFragment<{}>("routes-fragment");
55
+
56
+ const route1 = defineRoute({
57
+ method: "GET",
58
+ path: "/hello",
59
+ outputSchema: z.string(),
60
+ handler: async (_, { json }) => json("Hello"),
61
+ });
62
+
63
+ const route2 = defineRoute({
64
+ method: "GET",
65
+ path: "/goodbye",
66
+ outputSchema: z.string(),
67
+ handler: async (_, { json }) => json("Goodbye"),
68
+ });
69
+
70
+ const instance = instantiateFragment(fragment).withConfig({}).withRoutes([route1, route2]).build();
71
+
72
+ expect(instance.config.routes).toHaveLength(2);
73
+ expect(instance.config.routes[0].path).toBe("/hello");
74
+ expect(instance.config.routes[1].path).toBe("/goodbye");
75
+ ```
76
+
77
+ Routes can be added as an array using `withRoutes`.
78
+
79
+ ### Builder with Services and Dependencies
80
+
81
+ Also see the `fragment-services` subject.
82
+
83
+ ```typescript @fragno-test:builder-with-services
84
+ // should provide services and dependencies
85
+ interface AppConfig {
86
+ apiKey: string;
87
+ }
88
+
89
+ interface ILogger {
90
+ log(message: string): void;
91
+ }
92
+
93
+ const fragment = defineFragment<AppConfig>("service-fragment")
94
+ .withDependencies(({ config }) => ({
95
+ client: { key: config.apiKey },
96
+ }))
97
+ .usesService<"logger", ILogger>("logger");
98
+
99
+ const loggerImpl: ILogger = {
100
+ log: (msg) => console.log(msg),
101
+ };
102
+
103
+ const instance = instantiateFragment(fragment)
104
+ .withConfig({ apiKey: "my-key" })
105
+ .withServices({ logger: loggerImpl })
106
+ .build();
107
+
108
+ expect(instance.deps.client.key).toBe("my-key");
109
+ expect(instance.services.logger).toBeDefined();
110
+ ```
111
+
112
+ Use `withDependencies` to create dependencies from config, and `withServices` to provide required
113
+ services.
@@ -0,0 +1,178 @@
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, instantiateFragment } from "@fragno-dev/core";
11
+ import type { FragnoPublicConfig } from "@fragno-dev/core";
12
+ ```
13
+
14
+ ## Providing Services
15
+
16
+ Fragments provide services using `providesService()`. Services can be passed as a direct object or
17
+ via a factory function that receives context (`config`, `deps`, `fragnoConfig`, `defineService`).
18
+
19
+ ```typescript @fragno-test:provide-direct-object
20
+ // Object syntax is the simplest way to provide services.
21
+ const fragment = defineFragment<{}>("email-fragment").providesService({
22
+ sendEmail: async (to: string, subject: string) => {
23
+ // Email sending logic here
24
+ },
25
+ });
26
+ ```
27
+
28
+ ```typescript @fragno-test:provide-factory-function
29
+ // The factory function receives the fragment's `config`, and `deps` (dependencies).
30
+ const fragment = defineFragment<{}>("api-fragment").providesService(({ config }) => ({
31
+ makeRequest: async (endpoint: string) => {
32
+ return { data: "response" };
33
+ },
34
+ }));
35
+
36
+ const instance = instantiateFragment(fragment).withConfig({}).build();
37
+ expect(instance.services.makeRequest).toBeInstanceOf(Function);
38
+ ```
39
+
40
+ ### Named vs Unnamed Services
41
+
42
+ Unnamed services (shown above) are exposed directly on `instance.services` for direct user
43
+ consumption. Named services provide better organization and enable fragments to work together via
44
+ required services.
45
+
46
+ ```typescript @fragno-test:provide-named-services
47
+ // should provide named services
48
+ const fragment = defineFragment<{}>("logger-fragment").providesService(
49
+ "logger",
50
+ ({ defineService }) =>
51
+ defineService({
52
+ log: (message: string) => console.log(message),
53
+ error: (message: string) => console.error(message),
54
+ }),
55
+ );
56
+
57
+ const instance = instantiateFragment(fragment).withConfig({}).build();
58
+ expect(instance.services.logger.log).toBeDefined();
59
+ expect(instance.services.logger.error).toBeDefined();
60
+ ```
61
+
62
+ ### Using `defineService` helper method
63
+
64
+ Use `defineService` when service methods need proper `this` binding. This is particularly important
65
+ for database fragments where methods need access to the current unit of work.
66
+
67
+ ```typescript @fragno-test:chaining-services
68
+ // should chain multiple service definitions
69
+ const fragment = defineFragment<{}>("multi-service-fragment")
70
+ .providesService("logger", ({ defineService }) =>
71
+ defineService({
72
+ log: (msg: string) => console.log(msg),
73
+ }),
74
+ )
75
+ .providesService("validator", ({ defineService }) =>
76
+ defineService({
77
+ validate: (input: string) => input.length > 0,
78
+ }),
79
+ );
80
+ ```
81
+
82
+ ## Using Services
83
+
84
+ Fragments specify required services using `usesService`. Services can be marked as optional with
85
+ `{ optional: true }`, making them `undefined` if not provided.
86
+
87
+ ```typescript @fragno-test:declaring-required-service
88
+ // should require a service from the user
89
+ interface IEmailService {
90
+ sendEmail(to: string, subject: string, body: string): Promise<void>;
91
+ }
92
+
93
+ const fragment = defineFragment<{}>("notification-fragment").usesService<"email", IEmailService>(
94
+ "email",
95
+ );
96
+
97
+ const emailImpl: IEmailService = {
98
+ sendEmail: async (to, subject, body) => {
99
+ // Implementation
100
+ },
101
+ };
102
+
103
+ const instance = instantiateFragment(fragment)
104
+ .withConfig({})
105
+ .withServices({ email: emailImpl })
106
+ .build();
107
+
108
+ expect(instance.services.email).toBeDefined();
109
+ expect(instance.services.email.sendEmail).toBeDefined();
110
+ ```
111
+
112
+ ### Optional Services
113
+
114
+ ```typescript @fragno-test:optional-service
115
+ // should mark a service as optional
116
+ interface ILogger {
117
+ log(message: string): void;
118
+ }
119
+
120
+ const fragment = defineFragment<{}>("app-fragment").usesService<"logger", ILogger>("logger", {
121
+ optional: true,
122
+ });
123
+
124
+ // Can instantiate without providing the service
125
+ const instance = instantiateFragment(fragment).withConfig({}).build();
126
+
127
+ expect(instance).toBeDefined();
128
+ // logger will be undefined
129
+ expect(instance.services.logger).toBeUndefined();
130
+ ```
131
+
132
+ ### Using Services in Provided Services
133
+
134
+ Used services are available via `deps` in the factory function context, enabling composition.
135
+
136
+ ```typescript @fragno-test:using-in-provided
137
+ // should use external services in provided services
138
+ interface IEmailService {
139
+ sendEmail(to: string, subject: string, body: string): Promise<void>;
140
+ }
141
+
142
+ const fragment = defineFragment<{}>("welcome-fragment")
143
+ .usesService<"email", IEmailService>("email")
144
+ .providesService(({ deps }) => ({
145
+ sendWelcomeEmail: async (to: string) => {
146
+ await deps.email.sendEmail(to, "Welcome!", "Welcome to our service!");
147
+ },
148
+ }));
149
+
150
+ const emailImpl: IEmailService = {
151
+ sendEmail: async () => {},
152
+ };
153
+
154
+ const instance = instantiateFragment(fragment)
155
+ .withConfig({})
156
+ .withServices({ email: emailImpl })
157
+ .build();
158
+
159
+ expect(instance.services.sendWelcomeEmail).toBeDefined();
160
+ expect(instance.services.email).toBeDefined();
161
+ ```
162
+
163
+ ### Missing Required Services
164
+
165
+ ```typescript @fragno-test:missing-required-service
166
+ // should throw when required service not provided
167
+ interface IStorageService {
168
+ save(key: string, value: string): Promise<void>;
169
+ }
170
+
171
+ const fragment = defineFragment<{}>("storage-fragment").usesService<"storage", IStorageService>(
172
+ "storage",
173
+ );
174
+
175
+ expect(() => {
176
+ instantiateFragment(fragment).withConfig({}).build();
177
+ }).toThrow("Fragment 'storage-fragment' requires service 'storage' but it was not provided");
178
+ ```
@@ -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",
3
+ "version": "0.0.5",
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.6",
25
- "@fragno-dev/db": "0.1.13",
26
- "@fragno-dev/test": "0.1.10",
24
+ "@fragno-dev/core": "0.1.8",
25
+ "@fragno-dev/db": "0.1.14",
26
+ "@fragno-dev/test": "0.1.12",
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",