@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,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.4",
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.8",
25
- "@fragno-dev/db": "0.1.14",
26
- "@fragno-dev/test": "0.1.12",
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",