@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 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://chok.github.io/fougere/) (en/fr).
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 (`presentEgress`), so a route that re-applied it did the work twice — and
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 {
@@ -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,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"}
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, SchemaSource } from '@fougere/schema';
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 (`handler.operations` is core's `Map<string, OperationContract>`);
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?: SchemaSource;
36
- output?: SchemaSource;
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: SchemaSource;
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?: SchemaSource;
64
- /** The scanned constructor, which carries the same statement made on the class. */
65
- ctor?: {
66
- __output?: SchemaSource;
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: ''. */
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
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, inputFields as clientInputFields, outputFields as clientOutputFields } from '@fougere/schema';
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, overrides) {
14
- if (resolveIsReadOp(opName, overrides))
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
- // Use facade keys (includes inherited ops like CRUD), fallback to handler.operations
78
- const opNames = Object.keys(facade);
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 = handler?.operations.get(opName);
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, frond.operationsOverrides);
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 = clientOutputFields(fields);
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 = clientInputFields(fields);
111
+ inputFields = Visibility.of(fields).input;
101
112
  }
102
113
  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 */ }
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,
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
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.2.0-alpha.2",
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.2.0-alpha.2",
32
- "@fougere/schema": "0.2.0-alpha.2",
33
- "@fougere/http": "0.2.0-alpha.2"
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.
@@ -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
+ }