@fougere/adapter-rest 0.2.0-alpha.2 → 0.4.0-alpha.0
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 +1 -1
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/routes.d.ts +20 -17
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +27 -25
- package/dist/routes.js.map +1 -1
- package/package.json +6 -5
- package/src/index.ts +7 -0
- package/src/register.ts +62 -0
- package/src/routes.ts +288 -0
package/README.md
CHANGED
|
@@ -11,4 +11,4 @@ pnpm add @fougere/adapter-rest
|
|
|
11
11
|
|
|
12
12
|
Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
|
|
13
13
|
monolith to distributed, the same user code.
|
|
14
|
-
Reference documentation: [the site](https://
|
|
14
|
+
Reference documentation: [the site](https://fougere.dev/) (en/fr).
|
package/dist/register.js
CHANGED
|
@@ -30,7 +30,7 @@ export function registerRoutes(router, routes) {
|
|
|
30
30
|
return { status: 404, data: { error: 'Not found' } };
|
|
31
31
|
}
|
|
32
32
|
// The computed fields are already here: the façade applies the presenter on every
|
|
33
|
-
// door (`
|
|
33
|
+
// door (`PresenterExecutor`), so a route that re-applied it did the work twice — and
|
|
34
34
|
// once a computed field started receiving the PAGE rather than one row, the second
|
|
35
35
|
// pass handed it a single object and threw `posts.map is not a function`.
|
|
36
36
|
return {
|
package/dist/register.js.map
CHANGED
|
@@ -1 +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,
|
|
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,qFAAqF;gBACrF,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"}
|
package/dist/routes.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Framework-agnostic: produces RouteDefinition[], consumed by an adapter (Fastify, Express, etc).
|
|
5
5
|
*/
|
|
6
|
-
import type { Fields,
|
|
6
|
+
import type { Fields, SchemaOrCard } from '@fougere/schema';
|
|
7
7
|
import type { HandlerEntry as CoreHandlerEntry } from '@fougere/core';
|
|
8
8
|
export type { HttpMethod } from '@fougere/http';
|
|
9
9
|
import type { HttpMethod } from '@fougere/http';
|
|
@@ -24,7 +24,7 @@ export interface RouteDefinition {
|
|
|
24
24
|
* The operation in words — the method's own doc sentence, carried by the contract.
|
|
25
25
|
*
|
|
26
26
|
* A route is what an OpenAPI `summary` is generated FROM, and the sentence reached
|
|
27
|
-
* this file already
|
|
27
|
+
* this file already through core's `EffectiveOperation` table;
|
|
28
28
|
* only this type had no name for it, so every generated route was undocumented while
|
|
29
29
|
* the sentence sat one property away. Carried, not rendered: emitting OpenAPI is a
|
|
30
30
|
* reader's job, and this is what it reads.
|
|
@@ -32,15 +32,17 @@ export interface RouteDefinition {
|
|
|
32
32
|
description?: string;
|
|
33
33
|
}
|
|
34
34
|
interface OperationMeta {
|
|
35
|
-
input?:
|
|
36
|
-
output?:
|
|
35
|
+
input?: SchemaOrCard;
|
|
36
|
+
output?: SchemaOrCard;
|
|
37
|
+
/** Canonical kind from core's EffectiveOperation. */
|
|
38
|
+
kind: 'query' | 'command';
|
|
37
39
|
/** The operation in words — see `RouteDefinition.description`. */
|
|
38
40
|
description?: string;
|
|
39
41
|
}
|
|
40
42
|
interface EntityEntry {
|
|
41
43
|
name: string;
|
|
42
44
|
/** A live class in-process, a card from a frond whose class never crossed. */
|
|
43
|
-
entityClass:
|
|
45
|
+
entityClass: SchemaOrCard;
|
|
44
46
|
exposed?: boolean;
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
@@ -58,12 +60,18 @@ interface EntityEntry {
|
|
|
58
60
|
* package stay structurally typed. There the duplication is the doctrine, not an oversight.
|
|
59
61
|
*/
|
|
60
62
|
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface'> & {
|
|
61
|
-
operations: Map<string, OperationMeta>;
|
|
62
63
|
/** `Crud(Post, PostPublic)` — the handler-wide output view, scoping every op. */
|
|
63
|
-
outputOverride?:
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
outputOverride?: SchemaOrCard;
|
|
65
|
+
/**
|
|
66
|
+
* The scanned constructor, which carries the same statement made on the class.
|
|
67
|
+
*
|
|
68
|
+
* Typed as a constructor that MAY carry the static, not as `{ __output?: … }` alone:
|
|
69
|
+
* a type whose every property is optional is weak, and TS refuses a constructor that
|
|
70
|
+
* happens not to carry it — *has no properties in common*. So the narrow view was
|
|
71
|
+
* unassignable from the real entry for a second reason after `operations`.
|
|
72
|
+
*/
|
|
73
|
+
ctor?: (new (...args: never[]) => unknown) & {
|
|
74
|
+
__output?: SchemaOrCard;
|
|
67
75
|
};
|
|
68
76
|
};
|
|
69
77
|
interface PresenterEntry {
|
|
@@ -76,18 +84,13 @@ interface FrondLike {
|
|
|
76
84
|
handlers: HandlerEntry[];
|
|
77
85
|
presenters: PresenterEntry[];
|
|
78
86
|
surfaces?: Record<string, string[]>;
|
|
79
|
-
operationsOverrides?: Record<string, {
|
|
80
|
-
kind?: 'query' | 'command';
|
|
81
|
-
handlerName?: string;
|
|
82
|
-
method?: string;
|
|
83
|
-
policy?: string;
|
|
84
|
-
}>;
|
|
85
87
|
}
|
|
86
88
|
interface AppLike {
|
|
87
89
|
fronds: FrondLike[];
|
|
88
|
-
resolve<T>(name: string): T;
|
|
89
90
|
/** The façade an entity exposes to one audience — `undefined` when none. */
|
|
90
91
|
facadeFor(entity: string, surface?: string): Record<string, Function> | undefined;
|
|
92
|
+
/** Canonical operation table produced by core. */
|
|
93
|
+
operationsFor(entity: string, surface?: string): Map<string, OperationMeta> | undefined;
|
|
91
94
|
}
|
|
92
95
|
export interface GenerateRoutesOptions {
|
|
93
96
|
/** Base path prefix (e.g. '/api'). Default: ''. */
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAS,MAAM,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { fieldsOf,
|
|
2
|
-
import { resolveIsReadOp } from '@fougere/core';
|
|
1
|
+
import { fieldsOf, Visibility } from '@fougere/schema';
|
|
3
2
|
// ─── Naming conventions ─────────────────────────
|
|
4
3
|
function hasById(name) {
|
|
5
4
|
return name.includes('ById') || name === 'findById' || name === 'update' || name === 'delete';
|
|
@@ -10,8 +9,12 @@ function pluralize(name) {
|
|
|
10
9
|
: name + 's';
|
|
11
10
|
}
|
|
12
11
|
/** Derive HTTP method from operation name, honoring frond.config.ts overrides. */
|
|
13
|
-
function deriveMethod(opName,
|
|
14
|
-
if (
|
|
12
|
+
function deriveMethod(opName, resolvedKind) {
|
|
13
|
+
if (!resolvedKind) {
|
|
14
|
+
throw new Error(`REST cannot project '${opName}' without its resolved operation kind. `
|
|
15
|
+
+ 'Build routes from App.operationsFor(), the EffectiveOperation table produced by core.');
|
|
16
|
+
}
|
|
17
|
+
if (resolvedKind === 'query')
|
|
15
18
|
return 'GET';
|
|
16
19
|
if (opName.startsWith('create'))
|
|
17
20
|
return 'POST';
|
|
@@ -74,8 +77,13 @@ export function generateRoutes(app, options) {
|
|
|
74
77
|
const handler = (surfaceName
|
|
75
78
|
? frond.handlers.find((h) => h.address === entity.name && h.surface === surfaceName)
|
|
76
79
|
: undefined) ?? handlerMap.get(entity.name);
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
const effectiveOperations = app.operationsFor(entity.name, surfaceName);
|
|
81
|
+
if (!effectiveOperations) {
|
|
82
|
+
throw new Error(`REST cannot project '${entity.name}' without its EffectiveOperation table.`);
|
|
83
|
+
}
|
|
84
|
+
// The resolved table defines the public operation set. This also works for remote
|
|
85
|
+
// proxy facades, which intentionally cannot enumerate their keys before discovery.
|
|
86
|
+
const opNames = [...effectiveOperations.keys()];
|
|
79
87
|
const entityOverrides = overrides[entity.name] ?? {};
|
|
80
88
|
// Use handler's output schema if declared, otherwise entity. A live class or a
|
|
81
89
|
// card — `fieldsOf` takes both, so a frond whose class never crossed the wire
|
|
@@ -85,37 +93,31 @@ export function generateRoutes(app, options) {
|
|
|
85
93
|
?? entity.entityClass;
|
|
86
94
|
const fields = fieldsOf(outputSchema);
|
|
87
95
|
for (const opName of opNames) {
|
|
88
|
-
const meta =
|
|
96
|
+
const meta = effectiveOperations.get(opName);
|
|
97
|
+
if (!meta) {
|
|
98
|
+
throw new Error(`REST facade '${entity.name}' exposes '${opName}' but its EffectiveOperation table does not.`);
|
|
99
|
+
}
|
|
89
100
|
const override = entityOverrides[opName];
|
|
90
|
-
const method = override?.method ?? deriveMethod(opName,
|
|
101
|
+
const method = override?.method ?? deriveMethod(opName, meta?.kind);
|
|
91
102
|
const path = prefix + (override?.path ?? derivePath(entity.name, opName));
|
|
92
103
|
// Input/output fields: use meta if available, fallback to entity fields for CRUD.
|
|
93
104
|
// Both pass through the client-surface projections (write-only out, read-only in).
|
|
94
105
|
let inputFields;
|
|
95
|
-
let outputFields =
|
|
106
|
+
let outputFields = Visibility.of(fields).output;
|
|
96
107
|
if (meta?.input) {
|
|
97
108
|
inputFields = fieldsOf(meta.input);
|
|
98
109
|
}
|
|
99
110
|
else if (opName === 'create' || opName === 'update') {
|
|
100
|
-
inputFields =
|
|
111
|
+
inputFields = Visibility.of(fields).input;
|
|
101
112
|
}
|
|
102
113
|
if (meta?.output)
|
|
103
|
-
outputFields =
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
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 */ }
|
|
114
|
+
outputFields = Visibility.of(fieldsOf(meta.output)).output;
|
|
115
|
+
// Handler/method overrides are already executed by the facade from the same
|
|
116
|
+
// EffectiveOperation local and RPC use. The adapter never resolves DI itself.
|
|
117
|
+
const op = facade[opName];
|
|
118
|
+
if (typeof op !== 'function') {
|
|
119
|
+
throw new Error(`REST EffectiveOperation table exposes '${opName}' but its facade does not.`);
|
|
116
120
|
}
|
|
117
|
-
if (!op)
|
|
118
|
-
continue;
|
|
119
121
|
routes.push({
|
|
120
122
|
method,
|
|
121
123
|
path,
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/adapter-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-alpha.0",
|
|
4
4
|
"description": "Entity → REST routes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
29
30
|
],
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@fougere/core": "0.
|
|
32
|
-
"@fougere/
|
|
33
|
-
"@fougere/
|
|
32
|
+
"@fougere/core": "0.4.0-alpha.0",
|
|
33
|
+
"@fougere/http": "0.4.0-alpha.0",
|
|
34
|
+
"@fougere/schema": "0.4.0-alpha.0"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { generateRoutes, type RouteDefinition, type HttpMethod, type GenerateRoutesOptions } from './routes.js';
|
|
2
|
+
export { registerRoutes } from './register.js';
|
|
3
|
+
|
|
4
|
+
// No schema endpoint here. Discovery is `rpc.discover` on the envelope — one
|
|
5
|
+
// surface, which answers with what the host SERVES. A second GET published
|
|
6
|
+
// every scanned entity instead, façade or not: the auth tables of a host that
|
|
7
|
+
// mounted it were readable by anyone.
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic route registration — translates RouteDefinition[] into HttpRouter calls.
|
|
3
|
+
*/
|
|
4
|
+
import type { HttpRouter } from '@fougere/http';
|
|
5
|
+
import { toHttpError } from '@fougere/core';
|
|
6
|
+
import type { RouteDefinition } from './routes.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register all route definitions on an HttpRouter.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const routes = generateRoutes(app, { prefix: '/api' });
|
|
13
|
+
* registerRoutes(router, routes);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function registerRoutes(
|
|
17
|
+
router: HttpRouter,
|
|
18
|
+
routes: RouteDefinition[],
|
|
19
|
+
): void {
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
router.on(route.method, route.path, async (ctx) => {
|
|
22
|
+
const body = (await ctx.body()) as Record<string, unknown> | undefined;
|
|
23
|
+
|
|
24
|
+
// Build unified InvocationContext for the binding algorithm
|
|
25
|
+
const invocation = {
|
|
26
|
+
params: ctx.params,
|
|
27
|
+
query: ctx.query,
|
|
28
|
+
body,
|
|
29
|
+
state: ctx.state,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await route.handler(invocation);
|
|
34
|
+
|
|
35
|
+
if ((result === undefined || result === null) && route.operationName === 'findById') {
|
|
36
|
+
return { status: 404, data: { error: 'Not found' } };
|
|
37
|
+
}
|
|
38
|
+
if (result === true && route.operationName === 'delete') {
|
|
39
|
+
return { status: 204, data: null };
|
|
40
|
+
}
|
|
41
|
+
if (result === false && route.operationName === 'delete') {
|
|
42
|
+
return { status: 404, data: { error: 'Not found' } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The computed fields are already here: the façade applies the presenter on every
|
|
46
|
+
// door (`PresenterExecutor`), so a route that re-applied it did the work twice — and
|
|
47
|
+
// once a computed field started receiving the PAGE rather than one row, the second
|
|
48
|
+
// pass handed it a single object and threw `posts.map is not a function`.
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
status: route.successStatus ?? (route.operationName === 'create' ? 201 : 200),
|
|
52
|
+
// The façade is the single egress boundary for every door. Re-encoding here
|
|
53
|
+
// makes custom encoders run twice and makes REST disagree with the envelope.
|
|
54
|
+
data: result,
|
|
55
|
+
};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const { status, body } = toHttpError(err);
|
|
58
|
+
return { status, data: body };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
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 { Field, Fields, SchemaOrCard } from '@fougere/schema';
|
|
7
|
+
import { fieldsOf, Visibility } from '@fougere/schema';
|
|
8
|
+
import type { HandlerEntry as CoreHandlerEntry } from '@fougere/core';
|
|
9
|
+
|
|
10
|
+
// ─── Types ──────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
// Le vocabulaire des verbes appartient au routeur, pas à la projection : le redéclarer ici
|
|
13
|
+
// avait produit deux listes à tenir d'accord à la main, qui ont divergé au premier verbe ajouté.
|
|
14
|
+
export type { HttpMethod } from '@fougere/http';
|
|
15
|
+
import type { HttpMethod } from '@fougere/http';
|
|
16
|
+
|
|
17
|
+
export interface RouteDefinition {
|
|
18
|
+
method: HttpMethod;
|
|
19
|
+
path: string;
|
|
20
|
+
operationName: string;
|
|
21
|
+
entityName: string;
|
|
22
|
+
/** Handler facade method to call (receives InvocationContext). */
|
|
23
|
+
handler: (invocation: unknown) => Promise<unknown>;
|
|
24
|
+
/** Input schema (for validation / JSON schema generation). */
|
|
25
|
+
inputFields?: Fields;
|
|
26
|
+
/** Output schema (for JSON schema generation). */
|
|
27
|
+
outputFields?: Fields;
|
|
28
|
+
/** Explicit success status. Defaults to 201 only for the canonical `create` op. */
|
|
29
|
+
successStatus?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The operation in words — the method's own doc sentence, carried by the contract.
|
|
32
|
+
*
|
|
33
|
+
* A route is what an OpenAPI `summary` is generated FROM, and the sentence reached
|
|
34
|
+
* this file already through core's `EffectiveOperation` table;
|
|
35
|
+
* only this type had no name for it, so every generated route was undocumented while
|
|
36
|
+
* the sentence sat one property away. Carried, not rendered: emitting OpenAPI is a
|
|
37
|
+
* reader's job, and this is what it reads.
|
|
38
|
+
*/
|
|
39
|
+
description?: string;
|
|
40
|
+
// No presenter here. A route used to carry the instance and its field names so the
|
|
41
|
+
// registration could enrich each row; the façade does that for every door now
|
|
42
|
+
// (`PresenterExecutor`), so the rows arrive computed and a second pass was duplicated work.
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface OperationMeta {
|
|
46
|
+
input?: SchemaOrCard;
|
|
47
|
+
output?: SchemaOrCard;
|
|
48
|
+
/** Canonical kind from core's EffectiveOperation. */
|
|
49
|
+
kind: 'query' | 'command';
|
|
50
|
+
/** The operation in words — see `RouteDefinition.description`. */
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface EntityEntry {
|
|
55
|
+
name: string;
|
|
56
|
+
/** A live class in-process, a card from a frond whose class never crossed. */
|
|
57
|
+
entityClass: SchemaOrCard;
|
|
58
|
+
exposed?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Only what this projection reads of a scanned handler — five fields of nine.
|
|
63
|
+
*
|
|
64
|
+
* The narrowness is deliberate: a consumer that declares what it consumes accepts a
|
|
65
|
+
* minimal literal in a test and does not break when a field it ignores moves. What was
|
|
66
|
+
* NOT deliberate is that the two names it does read were spelled again here, so renaming
|
|
67
|
+
* `entityName` to `address` in core left this file compiling against a field the runtime
|
|
68
|
+
* object no longer carried — a silent break, caught only because the same commit touched
|
|
69
|
+
* the reads. `Pick` keeps the narrow view and makes core the one place the names live.
|
|
70
|
+
*
|
|
71
|
+
* `schema-graphql` holds a copy of this shape and cannot do the same: it depends on
|
|
72
|
+
* `@fougere/schema` and `@fougere/http`, never on core, which is what lets a projection
|
|
73
|
+
* package stay structurally typed. There the duplication is the doctrine, not an oversight.
|
|
74
|
+
*/
|
|
75
|
+
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface'> & {
|
|
76
|
+
/** `Crud(Post, PostPublic)` — the handler-wide output view, scoping every op. */
|
|
77
|
+
outputOverride?: SchemaOrCard;
|
|
78
|
+
/**
|
|
79
|
+
* The scanned constructor, which carries the same statement made on the class.
|
|
80
|
+
*
|
|
81
|
+
* Typed as a constructor that MAY carry the static, not as `{ __output?: … }` alone:
|
|
82
|
+
* a type whose every property is optional is weak, and TS refuses a constructor that
|
|
83
|
+
* happens not to carry it — *has no properties in common*. So the narrow view was
|
|
84
|
+
* unassignable from the real entry for a second reason after `operations`.
|
|
85
|
+
*/
|
|
86
|
+
ctor?: (new (...args: never[]) => unknown) & { __output?: SchemaOrCard };
|
|
87
|
+
};
|
|
88
|
+
// No `operations` here, and its absence is the point. It was declared, never read — this
|
|
89
|
+
// file takes its table from `app.operationsFor()` — and it carried `OperationMeta`, whose
|
|
90
|
+
// `kind` is REQUIRED because that is true of an EffectiveOperation. A scanned handler's
|
|
91
|
+
// contract leaves `kind` empty, so the narrow type was not a supertype of the real one and
|
|
92
|
+
// `generateRoutes(app)` did not typecheck against a real `App`. The framework's own caller
|
|
93
|
+
// wrote `generateRoutes(app as never, …)`, and the tests only ever passed narrow literals,
|
|
94
|
+
// so nothing caught it. Declaring what you consume means not declaring the rest.
|
|
95
|
+
|
|
96
|
+
interface PresenterEntry {
|
|
97
|
+
entityName: string;
|
|
98
|
+
fields: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface FrondLike {
|
|
102
|
+
name: string;
|
|
103
|
+
entities: EntityEntry[];
|
|
104
|
+
handlers: HandlerEntry[];
|
|
105
|
+
presenters: PresenterEntry[];
|
|
106
|
+
surfaces?: Record<string, string[]>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface AppLike {
|
|
110
|
+
fronds: FrondLike[];
|
|
111
|
+
/** The façade an entity exposes to one audience — `undefined` when none. */
|
|
112
|
+
facadeFor(entity: string, surface?: string): Record<string, Function> | undefined;
|
|
113
|
+
/** Canonical operation table produced by core. */
|
|
114
|
+
operationsFor(entity: string, surface?: string): Map<string, OperationMeta> | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type HandlerFacade = Record<string, Function>;
|
|
118
|
+
|
|
119
|
+
// ─── Naming conventions ─────────────────────────
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
function hasById(name: string): boolean {
|
|
123
|
+
return name.includes('ById') || name === 'findById' || name === 'update' || name === 'delete';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pluralize(name: string): string {
|
|
127
|
+
return name.endsWith('y')
|
|
128
|
+
? name.slice(0, -1) + 'ies'
|
|
129
|
+
: name + 's';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Derive HTTP method from operation name, honoring frond.config.ts overrides. */
|
|
133
|
+
function deriveMethod(
|
|
134
|
+
opName: string,
|
|
135
|
+
resolvedKind: OperationMeta['kind'],
|
|
136
|
+
): HttpMethod {
|
|
137
|
+
if (!resolvedKind) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`REST cannot project '${opName}' without its resolved operation kind. `
|
|
140
|
+
+ 'Build routes from App.operationsFor(), the EffectiveOperation table produced by core.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (resolvedKind === 'query') return 'GET';
|
|
144
|
+
if (opName.startsWith('create')) return 'POST';
|
|
145
|
+
if (opName.startsWith('update') || opName.startsWith('edit')) return 'PUT';
|
|
146
|
+
if (opName.startsWith('delete') || opName.startsWith('remove')) return 'DELETE';
|
|
147
|
+
return 'POST';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Derive route path from entity name + operation name. */
|
|
151
|
+
function derivePath(entityName: string, opName: string): string {
|
|
152
|
+
const base = `/${pluralize(entityName)}`;
|
|
153
|
+
|
|
154
|
+
// Standard CRUD
|
|
155
|
+
if (opName === 'list') return base;
|
|
156
|
+
if (opName === 'findById') return `${base}/:id`;
|
|
157
|
+
if (opName === 'create') return base;
|
|
158
|
+
if (opName === 'update') return `${base}/:id`;
|
|
159
|
+
if (opName === 'delete') return `${base}/:id`;
|
|
160
|
+
|
|
161
|
+
// Remaining operations (convention-based)
|
|
162
|
+
const withId = hasById(opName);
|
|
163
|
+
const cleanName = opName.replace('ById', '');
|
|
164
|
+
const segment = cleanName.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
165
|
+
|
|
166
|
+
return withId ? `${base}/:id/${segment}` : `${base}/${segment}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Route generation helpers ───────────────────
|
|
170
|
+
|
|
171
|
+
// Update routes carry the SAME fields: input omissibility is a projection of the
|
|
172
|
+
// route's MODE (operationName 'update' → patch), never forged per-field flags.
|
|
173
|
+
// Membership is the axes-derived `Visibility.input` projection from @fougere/schema.
|
|
174
|
+
|
|
175
|
+
// ─── Public API ─────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export interface GenerateRoutesOptions {
|
|
178
|
+
/** Base path prefix (e.g. '/api'). Default: ''. */
|
|
179
|
+
prefix?: string;
|
|
180
|
+
/** Override route config per entity. */
|
|
181
|
+
overrides?: Record<string, Record<string, { method?: HttpMethod; path?: string; status?: number }>>;
|
|
182
|
+
/** Filter entities. */
|
|
183
|
+
filter?: (entity: EntityEntry, frondName: string) => boolean;
|
|
184
|
+
/** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
|
|
185
|
+
surface?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate REST route definitions from a fougere App.
|
|
190
|
+
*
|
|
191
|
+
* Conventions:
|
|
192
|
+
* - Entity name → pluralized base path (/posts, /authors)
|
|
193
|
+
* - list → GET /posts
|
|
194
|
+
* - findById → GET /posts/:id
|
|
195
|
+
* - create → POST /posts
|
|
196
|
+
* - update → PUT /posts/:id
|
|
197
|
+
* - delete → DELETE /posts/:id
|
|
198
|
+
* - Custom: searchByTitle → GET /posts/search-by-title
|
|
199
|
+
* - Custom with ById: archiveById → POST /posts/:id/archive
|
|
200
|
+
* - Override any route via options.overrides
|
|
201
|
+
*/
|
|
202
|
+
export function generateRoutes(app: AppLike, options?: GenerateRoutesOptions): RouteDefinition[] {
|
|
203
|
+
const prefix = options?.prefix ?? '';
|
|
204
|
+
const overrides = options?.overrides ?? {};
|
|
205
|
+
const routes: RouteDefinition[] = [];
|
|
206
|
+
|
|
207
|
+
for (const frond of app.fronds) {
|
|
208
|
+
const handlerMap = new Map(frond.handlers.filter((h) => !h.surface).map((h) => [h.address, h]));
|
|
209
|
+
|
|
210
|
+
const surfaceName = options?.surface;
|
|
211
|
+
|
|
212
|
+
for (const entity of frond.entities) {
|
|
213
|
+
// Membership is core's answer, not ours — one rule, read here (see App.facadeFor).
|
|
214
|
+
const facade = app.facadeFor(entity.name, surfaceName) as HandlerFacade | undefined;
|
|
215
|
+
if (!facade) continue;
|
|
216
|
+
|
|
217
|
+
if (options?.filter && !options.filter(entity, frond.name)) continue;
|
|
218
|
+
if (!surfaceName && entity.exposed === false) continue;
|
|
219
|
+
|
|
220
|
+
const handler = (surfaceName
|
|
221
|
+
? frond.handlers.find((h) => h.address === entity.name && h.surface === surfaceName)
|
|
222
|
+
: undefined) ?? handlerMap.get(entity.name);
|
|
223
|
+
const effectiveOperations = app.operationsFor(entity.name, surfaceName);
|
|
224
|
+
if (!effectiveOperations) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`REST cannot project '${entity.name}' without its EffectiveOperation table.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
// The resolved table defines the public operation set. This also works for remote
|
|
230
|
+
// proxy facades, which intentionally cannot enumerate their keys before discovery.
|
|
231
|
+
const opNames = [...effectiveOperations.keys()];
|
|
232
|
+
const entityOverrides = overrides[entity.name] ?? {};
|
|
233
|
+
// Use handler's output schema if declared, otherwise entity. A live class or a
|
|
234
|
+
// card — `fieldsOf` takes both, so a frond whose class never crossed the wire
|
|
235
|
+
// projects the same routes as a local one.
|
|
236
|
+
const outputSchema: SchemaOrCard = handler?.outputOverride
|
|
237
|
+
?? handler?.ctor?.__output
|
|
238
|
+
?? entity.entityClass;
|
|
239
|
+
const fields = fieldsOf(outputSchema);
|
|
240
|
+
|
|
241
|
+
for (const opName of opNames) {
|
|
242
|
+
const meta = effectiveOperations.get(opName);
|
|
243
|
+
if (!meta) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`REST facade '${entity.name}' exposes '${opName}' but its EffectiveOperation table does not.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const override = entityOverrides[opName];
|
|
249
|
+
const method = override?.method ?? deriveMethod(opName, meta?.kind);
|
|
250
|
+
const path = prefix + (override?.path ?? derivePath(entity.name, opName));
|
|
251
|
+
|
|
252
|
+
// Input/output fields: use meta if available, fallback to entity fields for CRUD.
|
|
253
|
+
// Both pass through the client-surface projections (write-only out, read-only in).
|
|
254
|
+
let inputFields: Fields | undefined;
|
|
255
|
+
let outputFields: Fields | undefined = Visibility.of(fields).output;
|
|
256
|
+
if (meta?.input) {
|
|
257
|
+
inputFields = fieldsOf(meta.input);
|
|
258
|
+
} else if (opName === 'create' || opName === 'update') {
|
|
259
|
+
inputFields = Visibility.of(fields).input;
|
|
260
|
+
}
|
|
261
|
+
if (meta?.output) outputFields = Visibility.of(fieldsOf(meta.output)).output;
|
|
262
|
+
|
|
263
|
+
// Handler/method overrides are already executed by the facade from the same
|
|
264
|
+
// EffectiveOperation local and RPC use. The adapter never resolves DI itself.
|
|
265
|
+
const op = facade[opName];
|
|
266
|
+
if (typeof op !== 'function') {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`REST EffectiveOperation table exposes '${opName}' but its facade does not.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
routes.push({
|
|
273
|
+
method,
|
|
274
|
+
path,
|
|
275
|
+
operationName: opName,
|
|
276
|
+
entityName: entity.name,
|
|
277
|
+
handler: (invocation) => op(invocation),
|
|
278
|
+
inputFields,
|
|
279
|
+
outputFields,
|
|
280
|
+
successStatus: override?.status,
|
|
281
|
+
...(meta?.description && { description: meta.description }),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return routes;
|
|
288
|
+
}
|