@fougere/adapter-rest 0.2.0-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fougere contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @fougere/adapter-rest
2
+ > Entity → REST routes
3
+ The REST projection of a frond's operations, with egress applied at the edge.
4
+
5
+ ## Installation
6
+ ```bash
7
+ pnpm add @fougere/adapter-rest
8
+ ```
9
+
10
+ ---
11
+
12
+ Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
13
+ monolith to distributed, the same user code.
14
+ Reference documentation: [the site](https://chok.github.io/fougere/) (en/fr).
@@ -0,0 +1,3 @@
1
+ export { generateRoutes, type RouteDefinition, type HttpMethod, type GenerateRoutesOptions } from './routes.js';
2
+ export { registerRoutes } from './register.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAChH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { generateRoutes } from './routes.js';
2
+ export { registerRoutes } from './register.js';
3
+ // No schema endpoint here. Discovery is `rpc.discover` on the envelope — one
4
+ // surface, which answers with what the host SERVES. A second GET published
5
+ // every scanned entity instead, façade or not: the auth tables of a host that
6
+ // mounted it were readable by anyone.
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAqE,MAAM,aAAa,CAAC;AAChH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,sCAAsC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Framework-agnostic route registration — translates RouteDefinition[] into HttpRouter calls.
3
+ */
4
+ import type { HttpRouter } from '@fougere/http';
5
+ import type { RouteDefinition } from './routes.js';
6
+ /**
7
+ * Register all route definitions on an HttpRouter.
8
+ *
9
+ * ```ts
10
+ * const routes = generateRoutes(app, { prefix: '/api' });
11
+ * registerRoutes(router, routes);
12
+ * ```
13
+ */
14
+ export declare function registerRoutes(router: HttpRouter, routes: RouteDefinition[]): void;
15
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,eAAe,EAAE,GACxB,IAAI,CA2CN"}
@@ -0,0 +1,50 @@
1
+ import { toHttpError } from '@fougere/core';
2
+ /**
3
+ * Register all route definitions on an HttpRouter.
4
+ *
5
+ * ```ts
6
+ * const routes = generateRoutes(app, { prefix: '/api' });
7
+ * registerRoutes(router, routes);
8
+ * ```
9
+ */
10
+ export function registerRoutes(router, routes) {
11
+ for (const route of routes) {
12
+ router.on(route.method, route.path, async (ctx) => {
13
+ const body = (await ctx.body());
14
+ // Build unified InvocationContext for the binding algorithm
15
+ const invocation = {
16
+ params: ctx.params,
17
+ query: ctx.query,
18
+ body,
19
+ state: ctx.state,
20
+ };
21
+ try {
22
+ const result = await route.handler(invocation);
23
+ if ((result === undefined || result === null) && route.operationName === 'findById') {
24
+ return { status: 404, data: { error: 'Not found' } };
25
+ }
26
+ if (result === true && route.operationName === 'delete') {
27
+ return { status: 204, data: null };
28
+ }
29
+ if (result === false && route.operationName === 'delete') {
30
+ return { status: 404, data: { error: 'Not found' } };
31
+ }
32
+ // The computed fields are already here: the façade applies the presenter on every
33
+ // door (`presentEgress`), so a route that re-applied it did the work twice — and
34
+ // once a computed field started receiving the PAGE rather than one row, the second
35
+ // pass handed it a single object and threw `posts.map is not a function`.
36
+ return {
37
+ status: route.successStatus ?? (route.operationName === 'create' ? 201 : 200),
38
+ // The façade is the single egress boundary for every door. Re-encoding here
39
+ // makes custom encoders run twice and makes REST disagree with the envelope.
40
+ data: result,
41
+ };
42
+ }
43
+ catch (err) {
44
+ const { status, body } = toHttpError(err);
45
+ return { status, data: body };
46
+ }
47
+ });
48
+ }
49
+ }
50
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAkB,EAClB,MAAyB;IAEzB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAChD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwC,CAAC;YAEvE,4DAA4D;YAC5D,MAAM,UAAU,GAAG;gBACjB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,IAAI;gBACJ,KAAK,EAAE,GAAG,CAAC,KAAK;aACjB,CAAC;YAEF,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;gBAE/C,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,CAAC,IAAI,KAAK,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;oBACpF,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC;gBACvD,CAAC;gBACD,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;oBACxD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBACrC,CAAC;gBACD,IAAI,MAAM,KAAK,KAAK,IAAI,KAAK,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;oBACzD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC;gBACvD,CAAC;gBAED,kFAAkF;gBAClF,iFAAiF;gBACjF,mFAAmF;gBACnF,0EAA0E;gBAE1E,OAAO;oBACL,MAAM,EAAE,KAAK,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;oBAC7E,4EAA4E;oBAC5E,6EAA6E;oBAC7E,IAAI,EAAE,MAAM;iBACb,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;gBAC1C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChC,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @fougere/adapter-rest — generates REST route definitions from fougere handlers.
3
+ *
4
+ * Framework-agnostic: produces RouteDefinition[], consumed by an adapter (Fastify, Express, etc).
5
+ */
6
+ import type { Fields, SchemaSource } from '@fougere/schema';
7
+ import type { HandlerEntry as CoreHandlerEntry } from '@fougere/core';
8
+ export type { HttpMethod } from '@fougere/http';
9
+ import type { HttpMethod } from '@fougere/http';
10
+ export interface RouteDefinition {
11
+ method: HttpMethod;
12
+ path: string;
13
+ operationName: string;
14
+ entityName: string;
15
+ /** Handler facade method to call (receives InvocationContext). */
16
+ handler: (invocation: unknown) => Promise<unknown>;
17
+ /** Input schema (for validation / JSON schema generation). */
18
+ inputFields?: Fields;
19
+ /** Output schema (for JSON schema generation). */
20
+ outputFields?: Fields;
21
+ /** Explicit success status. Defaults to 201 only for the canonical `create` op. */
22
+ successStatus?: number;
23
+ /**
24
+ * The operation in words — the method's own doc sentence, carried by the contract.
25
+ *
26
+ * A route is what an OpenAPI `summary` is generated FROM, and the sentence reached
27
+ * this file already (`handler.operations` is core's `Map<string, OperationContract>`);
28
+ * only this type had no name for it, so every generated route was undocumented while
29
+ * the sentence sat one property away. Carried, not rendered: emitting OpenAPI is a
30
+ * reader's job, and this is what it reads.
31
+ */
32
+ description?: string;
33
+ }
34
+ interface OperationMeta {
35
+ input?: SchemaSource;
36
+ output?: SchemaSource;
37
+ /** The operation in words — see `RouteDefinition.description`. */
38
+ description?: string;
39
+ }
40
+ interface EntityEntry {
41
+ name: string;
42
+ /** A live class in-process, a card from a frond whose class never crossed. */
43
+ entityClass: SchemaSource;
44
+ exposed?: boolean;
45
+ }
46
+ /**
47
+ * Only what this projection reads of a scanned handler — five fields of nine.
48
+ *
49
+ * The narrowness is deliberate: a consumer that declares what it consumes accepts a
50
+ * minimal literal in a test and does not break when a field it ignores moves. What was
51
+ * NOT deliberate is that the two names it does read were spelled again here, so renaming
52
+ * `entityName` to `address` in core left this file compiling against a field the runtime
53
+ * object no longer carried — a silent break, caught only because the same commit touched
54
+ * the reads. `Pick` keeps the narrow view and makes core the one place the names live.
55
+ *
56
+ * `schema-graphql` holds a copy of this shape and cannot do the same: it depends on
57
+ * `@fougere/schema` and `@fougere/http`, never on core, which is what lets a projection
58
+ * package stay structurally typed. There the duplication is the doctrine, not an oversight.
59
+ */
60
+ type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface'> & {
61
+ operations: Map<string, OperationMeta>;
62
+ /** `Crud(Post, PostPublic)` — the handler-wide output view, scoping every op. */
63
+ outputOverride?: SchemaSource;
64
+ /** The scanned constructor, which carries the same statement made on the class. */
65
+ ctor?: {
66
+ __output?: SchemaSource;
67
+ };
68
+ };
69
+ interface PresenterEntry {
70
+ entityName: string;
71
+ fields: string[];
72
+ }
73
+ interface FrondLike {
74
+ name: string;
75
+ entities: EntityEntry[];
76
+ handlers: HandlerEntry[];
77
+ presenters: PresenterEntry[];
78
+ surfaces?: Record<string, string[]>;
79
+ operationsOverrides?: Record<string, {
80
+ kind?: 'query' | 'command';
81
+ handlerName?: string;
82
+ method?: string;
83
+ policy?: string;
84
+ }>;
85
+ }
86
+ interface AppLike {
87
+ fronds: FrondLike[];
88
+ resolve<T>(name: string): T;
89
+ /** The façade an entity exposes to one audience — `undefined` when none. */
90
+ facadeFor(entity: string, surface?: string): Record<string, Function> | undefined;
91
+ }
92
+ export interface GenerateRoutesOptions {
93
+ /** Base path prefix (e.g. '/api'). Default: ''. */
94
+ prefix?: string;
95
+ /** Override route config per entity. */
96
+ overrides?: Record<string, Record<string, {
97
+ method?: HttpMethod;
98
+ path?: string;
99
+ status?: number;
100
+ }>>;
101
+ /** Filter entities. */
102
+ filter?: (entity: EntityEntry, frondName: string) => boolean;
103
+ /** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
104
+ surface?: string;
105
+ }
106
+ /**
107
+ * Generate REST route definitions from a fougere App.
108
+ *
109
+ * Conventions:
110
+ * - Entity name → pluralized base path (/posts, /authors)
111
+ * - list → GET /posts
112
+ * - findById → GET /posts/:id
113
+ * - create → POST /posts
114
+ * - update → PUT /posts/:id
115
+ * - delete → DELETE /posts/:id
116
+ * - Custom: searchByTitle → GET /posts/search-by-title
117
+ * - Custom with ById: archiveById → POST /posts/:id/archive
118
+ * - Override any route via options.overrides
119
+ */
120
+ export declare function generateRoutes(app: AppLike, options?: GenerateRoutesOptions): RouteDefinition[];
121
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
package/dist/routes.js ADDED
@@ -0,0 +1,135 @@
1
+ import { fieldsOf, inputFields as clientInputFields, outputFields as clientOutputFields } from '@fougere/schema';
2
+ import { resolveIsReadOp } from '@fougere/core';
3
+ // ─── Naming conventions ─────────────────────────
4
+ function hasById(name) {
5
+ return name.includes('ById') || name === 'findById' || name === 'update' || name === 'delete';
6
+ }
7
+ function pluralize(name) {
8
+ return name.endsWith('y')
9
+ ? name.slice(0, -1) + 'ies'
10
+ : name + 's';
11
+ }
12
+ /** Derive HTTP method from operation name, honoring frond.config.ts overrides. */
13
+ function deriveMethod(opName, overrides) {
14
+ if (resolveIsReadOp(opName, overrides))
15
+ return 'GET';
16
+ if (opName.startsWith('create'))
17
+ return 'POST';
18
+ if (opName.startsWith('update') || opName.startsWith('edit'))
19
+ return 'PUT';
20
+ if (opName.startsWith('delete') || opName.startsWith('remove'))
21
+ return 'DELETE';
22
+ return 'POST';
23
+ }
24
+ /** Derive route path from entity name + operation name. */
25
+ function derivePath(entityName, opName) {
26
+ const base = `/${pluralize(entityName)}`;
27
+ // Standard CRUD
28
+ if (opName === 'list')
29
+ return base;
30
+ if (opName === 'findById')
31
+ return `${base}/:id`;
32
+ if (opName === 'create')
33
+ return base;
34
+ if (opName === 'update')
35
+ return `${base}/:id`;
36
+ if (opName === 'delete')
37
+ return `${base}/:id`;
38
+ // Remaining operations (convention-based)
39
+ const withId = hasById(opName);
40
+ const cleanName = opName.replace('ById', '');
41
+ const segment = cleanName.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
42
+ return withId ? `${base}/:id/${segment}` : `${base}/${segment}`;
43
+ }
44
+ /**
45
+ * Generate REST route definitions from a fougere App.
46
+ *
47
+ * Conventions:
48
+ * - Entity name → pluralized base path (/posts, /authors)
49
+ * - list → GET /posts
50
+ * - findById → GET /posts/:id
51
+ * - create → POST /posts
52
+ * - update → PUT /posts/:id
53
+ * - delete → DELETE /posts/:id
54
+ * - Custom: searchByTitle → GET /posts/search-by-title
55
+ * - Custom with ById: archiveById → POST /posts/:id/archive
56
+ * - Override any route via options.overrides
57
+ */
58
+ export function generateRoutes(app, options) {
59
+ const prefix = options?.prefix ?? '';
60
+ const overrides = options?.overrides ?? {};
61
+ const routes = [];
62
+ for (const frond of app.fronds) {
63
+ const handlerMap = new Map(frond.handlers.filter((h) => !h.surface).map((h) => [h.address, h]));
64
+ const surfaceName = options?.surface;
65
+ for (const entity of frond.entities) {
66
+ // Membership is core's answer, not ours — one rule, read here (see App.facadeFor).
67
+ const facade = app.facadeFor(entity.name, surfaceName);
68
+ if (!facade)
69
+ continue;
70
+ if (options?.filter && !options.filter(entity, frond.name))
71
+ continue;
72
+ if (!surfaceName && entity.exposed === false)
73
+ continue;
74
+ const handler = (surfaceName
75
+ ? frond.handlers.find((h) => h.address === entity.name && h.surface === surfaceName)
76
+ : undefined) ?? handlerMap.get(entity.name);
77
+ // Use facade keys (includes inherited ops like CRUD), fallback to handler.operations
78
+ const opNames = Object.keys(facade);
79
+ const entityOverrides = overrides[entity.name] ?? {};
80
+ // Use handler's output schema if declared, otherwise entity. A live class or a
81
+ // card — `fieldsOf` takes both, so a frond whose class never crossed the wire
82
+ // projects the same routes as a local one.
83
+ const outputSchema = handler?.outputOverride
84
+ ?? handler?.ctor?.__output
85
+ ?? entity.entityClass;
86
+ const fields = fieldsOf(outputSchema);
87
+ for (const opName of opNames) {
88
+ const meta = handler?.operations.get(opName);
89
+ const override = entityOverrides[opName];
90
+ const method = override?.method ?? deriveMethod(opName, frond.operationsOverrides);
91
+ const path = prefix + (override?.path ?? derivePath(entity.name, opName));
92
+ // Input/output fields: use meta if available, fallback to entity fields for CRUD.
93
+ // Both pass through the client-surface projections (write-only out, read-only in).
94
+ let inputFields;
95
+ let outputFields = clientOutputFields(fields);
96
+ if (meta?.input) {
97
+ inputFields = fieldsOf(meta.input);
98
+ }
99
+ else if (opName === 'create' || opName === 'update') {
100
+ inputFields = clientInputFields(fields);
101
+ }
102
+ if (meta?.output)
103
+ outputFields = clientOutputFields(fieldsOf(meta.output));
104
+ // Per-op handler override: redirect to a different class+method resolved from DI.
105
+ const opOverride = frond.operationsOverrides?.[opName];
106
+ let op = facade[opName];
107
+ if (opOverride?.handlerName) {
108
+ try {
109
+ const altFacade = app.resolve(opOverride.handlerName);
110
+ const altMethod = opOverride.method ?? opName;
111
+ if (typeof altFacade[altMethod] === 'function') {
112
+ op = altFacade[altMethod].bind(altFacade);
113
+ }
114
+ }
115
+ catch { /* alternate handler not in DI — fall back to default */ }
116
+ }
117
+ if (!op)
118
+ continue;
119
+ routes.push({
120
+ method,
121
+ path,
122
+ operationName: opName,
123
+ entityName: entity.name,
124
+ handler: (invocation) => op(invocation),
125
+ inputFields,
126
+ outputFields,
127
+ successStatus: override?.status,
128
+ ...(meta?.description && { description: meta.description }),
129
+ });
130
+ }
131
+ }
132
+ }
133
+ return routes;
134
+ }
135
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@fougere/adapter-rest",
3
+ "version": "0.2.0-alpha.2",
4
+ "description": "Entity → REST routes.",
5
+ "keywords": [
6
+ "fougere",
7
+ "typescript",
8
+ "rest",
9
+ "http",
10
+ "api"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/chok/fougere.git",
16
+ "directory": "packages/adapter/rest"
17
+ },
18
+ "type": "module",
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "dependencies": {
31
+ "@fougere/core": "0.2.0-alpha.2",
32
+ "@fougere/schema": "0.2.0-alpha.2",
33
+ "@fougere/http": "0.2.0-alpha.2"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "rm -rf dist && tsc",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
43
+ }
44
+ }