@fougere/adapter-rest 0.20.0-alpha.0 → 0.21.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.
@@ -1,10 +1,41 @@
1
1
  import { Format, type Accepted } from '@fougere/schema';
2
- /** What `protocols: { rest: … }` may say — nothing yet, so nothing is admitted. */
3
- declare const REST: Format<object>;
2
+ import { type ProtocolConfig } from '@fougere/core';
3
+ /** Where one operation answers, when what its contract derives is not what it should be. */
4
+ declare const ROUTE: Format<{
5
+ method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT" | undefined;
6
+ path?: string | undefined;
7
+ status?: number | undefined;
8
+ }>;
9
+ /**
10
+ * Where the app mounts REST, and the routes of a frond it cannot edit — `betterAuth()`, a package's.
11
+ * A frond of this project says its own in its `frond.config.ts`.
12
+ */
13
+ declare const REST: Format<{
14
+ overrides?: Record<string, {
15
+ method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT" | undefined;
16
+ path?: string | undefined;
17
+ status?: number | undefined;
18
+ }> | undefined;
19
+ prefix?: string | undefined;
20
+ }>;
21
+ /** What a frond says of itself: the segment it answers under, `/journal` rather than `/blog`. */
22
+ declare const FROND: Format<{
23
+ path?: string | undefined;
24
+ }>;
25
+ export type RestRoute = Accepted<typeof ROUTE>;
4
26
  declare module '@fougere/core' {
5
27
  interface FougereProtocols {
6
28
  rest: Accepted<typeof REST>;
7
29
  }
30
+ interface FougereOperationProtocols {
31
+ rest?: RestRoute;
32
+ }
33
+ interface FougereFrondProtocols {
34
+ rest?: Accepted<typeof FROND>;
35
+ }
8
36
  }
37
+ /** Where REST is mounted when `protocols.rest.prefix` says nothing. */
38
+ export declare const PREFIX = "/api";
39
+ export declare function prefixOf(protocols: ProtocolConfig | undefined): string;
9
40
  export {};
10
41
  //# sourceMappingURL=RestProtocol.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RestProtocol.d.ts","sourceRoot":"","sources":["../src/RestProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGxD,mFAAmF;AACnF,QAAA,MAAM,IAAI,gBAAsC,CAAC;AAEjD,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,gBAAgB;QACxB,IAAI,EAAE,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;KAC7B;CACF"}
1
+ {"version":3,"file":"RestProtocol.d.ts","sourceRoot":"","sources":["../src/RestProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,EAA4D,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE9G,4FAA4F;AAC5F,QAAA,MAAM,KAAK;;;;EAIA,CAAC;AAEZ;;;GAGG;AACH,QAAA,MAAM,IAAI;;;;;;;EAGC,CAAC;AAEZ,iGAAiG;AACjG,QAAA,MAAM,KAAK;;EAEA,CAAC;AAEZ,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,KAAK,CAAC,CAAC;AAE/C,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,gBAAgB;QACxB,IAAI,EAAE,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;KAC7B;IACD,UAAU,yBAAyB;QACjC,IAAI,CAAC,EAAE,SAAS,CAAC;KAClB;IACD,UAAU,qBAAqB;QAC7B,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,KAAK,CAAC,CAAC;KAC/B;CACF;AAED,uEAAuE;AACvE,eAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,wBAAgB,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,SAAS,GAAG,MAAM,CAEtE"}
@@ -1,6 +1,48 @@
1
1
  import { Format } from '@fougere/schema';
2
- import { Protocols } from '@fougere/core';
3
- /** What `protocols: { rest: … }` may say — nothing yet, so nothing is admitted. */
4
- const REST = Format.of('protocol/rest').closed();
5
- Protocols.register('rest', { format: REST });
2
+ import { HTTP_METHODS } from '@fougere/http';
3
+ import { Protocols, RouteAddress, isScanned } from '@fougere/core';
4
+ /** Where one operation answers, when what its contract derives is not what it should be. */
5
+ const ROUTE = Format.of('protocol/rest/route')
6
+ .key('method', Format.tokens(HTTP_METHODS))
7
+ .key('path', Format.text)
8
+ .key('status', Format.count)
9
+ .closed();
10
+ /**
11
+ * Where the app mounts REST, and the routes of a frond it cannot edit — `betterAuth()`, a package's.
12
+ * A frond of this project says its own in its `frond.config.ts`.
13
+ */
14
+ const REST = Format.of('protocol/rest')
15
+ .key('prefix', Format.text)
16
+ .key('overrides', Format.recordOf(ROUTE))
17
+ .closed();
18
+ /** What a frond says of itself: the segment it answers under, `/journal` rather than `/blog`. */
19
+ const FROND = Format.of('protocol/rest/frond')
20
+ .key('path', Format.text)
21
+ .closed();
22
+ /** Where REST is mounted when `protocols.rest.prefix` says nothing. */
23
+ export const PREFIX = '/api';
24
+ export function prefixOf(protocols) {
25
+ return protocols?.rest?.prefix ?? PREFIX;
26
+ }
27
+ /** An override of `fougere.config.ts` that a `frond.config.ts` should have said, or that names no op. */
28
+ function misplaced(overrides, fronds) {
29
+ return Object.entries(overrides ?? {}).flatMap(([key, route]) => {
30
+ const named = RouteAddress.parse(key);
31
+ if (!named)
32
+ return [`overrides: '${key}' names no address. An op is named '<address>.<op>'.`];
33
+ const owner = fronds.find((frond) => frond.handlers.some((handler) => handler.address === named.address));
34
+ if (!owner)
35
+ return [`overrides: '${key}' — no frond serves the address '${named.address}'.`];
36
+ if (!isScanned(owner))
37
+ return [];
38
+ return [
39
+ `overrides: '${key}' belongs to ${owner.name}, whose frond.config.ts is in this project — state it there:\n`
40
+ + ` ${owner.source.path}/frond.config.ts → operations: { '${key}': { rest: ${JSON.stringify(route)} } }`,
41
+ ];
42
+ });
43
+ }
44
+ Protocols.register('rest', {
45
+ format: REST,
46
+ refusals: (options, fronds) => misplaced(options.overrides, fronds),
47
+ });
6
48
  //# sourceMappingURL=RestProtocol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RestProtocol.js","sourceRoot":"","sources":["../src/RestProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,mFAAmF;AACnF,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,CAAC;AAQjD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"RestProtocol.js","sourceRoot":"","sources":["../src/RestProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAA6C,MAAM,eAAe,CAAC;AAE9G,4FAA4F;AAC5F,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC;KAC3C,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;KAC1C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC;KACxB,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC;KAC3B,MAAM,EAAE,CAAC;AAEZ;;;GAGG;AACH,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,eAAe,CAAC;KACpC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC;KAC1B,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;KACxC,MAAM,EAAE,CAAC;AAEZ,iGAAiG;AACjG,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC;KAC3C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC;KACxB,MAAM,EAAE,CAAC;AAgBZ,uEAAuE;AACvE,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC;AAE7B,MAAM,UAAU,QAAQ,CAAC,SAAqC;IAC5D,OAAO,SAAS,EAAE,IAAI,EAAE,MAAM,IAAI,MAAM,CAAC;AAC3C,CAAC;AAED,yGAAyG;AACzG,SAAS,SAAS,CAAC,SAAgD,EAAE,MAAkC;IACrG,OAAO,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC9D,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,eAAe,GAAG,sDAAsD,CAAC,CAAC;QAE9F,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1G,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,eAAe,GAAG,oCAAoC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;QAC7F,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEjC,OAAO;YACL,eAAe,GAAG,gBAAgB,KAAK,CAAC,IAAI,gEAAgE;kBACxG,KAAK,KAAK,CAAC,MAAM,CAAC,IAAI,qCAAqC,GAAG,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM;SAC5G,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE;IACzB,MAAM,EAAE,IAAI;IACZ,QAAQ,EAAE,CAAC,OAA8B,EAAE,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3F,CAAC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import './RestProtocol.js';
2
2
  export { generateRoutes, type RouteDefinition } from './routes/RouteDefinition.js';
3
3
  export type { HttpMethod } from '@fougere/http';
4
+ export { PREFIX, prefixOf, type RestRoute } from './RestProtocol.js';
4
5
  export { registerRoutes } from './register.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnF,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnF,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import './RestProtocol.js';
2
2
  export { generateRoutes } from './routes/RouteDefinition.js';
3
+ export { PREFIX, prefixOf } from './RestProtocol.js';
3
4
  export { registerRoutes } from './register.js';
4
5
  // No schema endpoint here. Discovery is `rpc.discover` on the envelope — one
5
6
  // surface, which answers with what the host SERVES. A second GET published
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAwB,MAAM,6BAA6B,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,sCAAsC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAwB,MAAM,6BAA6B,CAAC;AAEnF,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAkB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,sCAAsC"}
@@ -1,13 +1,4 @@
1
- import type { HttpMethod } from '@fougere/http';
2
1
  export interface GenerateRoutesOptions {
3
- /** Base path prefix (e.g. '/api'). Default: ''. */
4
- prefix?: string;
5
- /** Override route config per address, then per operation. */
6
- overrides?: Record<string, Record<string, {
7
- method?: HttpMethod;
8
- path?: string;
9
- status?: number;
10
- }>>;
11
2
  /** Keep only the addresses this answers true for. */
12
3
  filter?: (address: string, frondName: string) => boolean;
13
4
  /** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
@@ -1 +1 @@
1
- {"version":3,"file":"GenerateRoutesOptions.d.ts","sourceRoot":"","sources":["../../src/routes/GenerateRoutesOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACpG,qDAAqD;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACzD,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"GenerateRoutesOptions.d.ts","sourceRoot":"","sources":["../../src/routes/GenerateRoutesOptions.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,qDAAqD;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACzD,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -1,4 +1,4 @@
1
1
  import type { EffectiveOperation } from '@fougere/core';
2
2
  /** What a route reads of an effective operation — its `binding` is what the path is derived from. */
3
- export type OperationMeta = Pick<EffectiveOperation, 'input' | 'output' | 'kind' | 'description'> & Partial<Pick<EffectiveOperation, 'binding'>>;
3
+ export type OperationMeta = Pick<EffectiveOperation, 'input' | 'output' | 'kind' | 'description'> & Partial<Pick<EffectiveOperation, 'binding' | 'protocols'>>;
4
4
  //# sourceMappingURL=OperationMeta.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OperationMeta.d.ts","sourceRoot":"","sources":["../../src/routes/OperationMeta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,qGAAqG;AACrG,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAC,GAC7F,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"OperationMeta.d.ts","sourceRoot":"","sources":["../../src/routes/OperationMeta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,qGAAqG;AACrG,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAC,GAC7F,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import type { Fields } from '@fougere/schema';
2
2
  import type { HttpMethod } from '@fougere/http';
3
- import type { HandlerEntry as CoreHandlerEntry, OperationOverride } from '@fougere/core';
3
+ import { type FougereFrondProtocols, type HandlerEntry as CoreHandlerEntry, type ProtocolConfig } from '@fougere/core';
4
4
  import type { OperationMeta } from './OperationMeta.js';
5
5
  import type { GenerateRoutesOptions } from './GenerateRoutesOptions.js';
6
6
  export interface RouteDefinition {
@@ -31,11 +31,12 @@ interface FrondLike {
31
31
  handlers: HandlerEntry[];
32
32
  presenters: PresenterEntry[];
33
33
  surfaces?: Record<string, string[]>;
34
- /** What `frond.config.ts` said per op — `rest:` is read here, `graphql:` next facade. */
35
- operationsOverrides?: Record<string, Pick<OperationOverride, 'rest'>>;
34
+ protocols?: FougereFrondProtocols;
36
35
  }
37
36
  interface AppLike {
38
37
  fronds: FrondLike[];
38
+ /** What `fougere.config.ts` says to each protocol — where REST is mounted, a third party's routes. */
39
+ protocols?: ProtocolConfig;
39
40
  /** The façade an address serves to one audience — `undefined` when none. */
40
41
  facadeFor(address: string, surface?: string): Record<string, Function> | undefined;
41
42
  /** Canonical operation table produced by core. */
@@ -1 +1 @@
1
- {"version":3,"file":"RouteDefinition.d.ts","sourceRoot":"","sources":["../../src/routes/RouteDefinition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,IAAI,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,OAAO,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;CAItB;AAED,4DAA4D;AAC5D,KAAK,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC,CAAC;AAS9E,UAAU,cAAc;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;CACvE;AAED,UAAU,OAAO;IACf,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,4EAA4E;IAC5E,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IACnF,kDAAkD;IAClD,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,SAAS,CAAC;CAC1F;AA6DD,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,qBAA0B,GAAG,eAAe,EAAE,CAInG"}
1
+ {"version":3,"file":"RouteDefinition.d.ts","sourceRoot":"","sources":["../../src/routes/RouteDefinition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAyB,KAAK,qBAAqB,EAAE,KAAK,YAAY,IAAI,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE9I,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGxE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,OAAO,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;CAItB;AAED,4DAA4D;AAC5D,KAAK,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC,CAAC;AAS9E,UAAU,cAAc;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAED,UAAU,OAAO;IACf,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,sGAAsG;IACtG,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,4EAA4E;IAC5E,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IACnF,kDAAkD;IAClD,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,SAAS,CAAC;CAC1F;AA4FD,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,qBAA0B,GAAG,eAAe,EAAE,CAInG"}
@@ -1,4 +1,6 @@
1
- import { Visibility } from '@fougere/schema';
1
+ import { Card, Visibility } from '@fougere/schema';
2
+ import { RouteAddress, wordsOf } from '@fougere/core';
3
+ import { prefixOf } from '../RestProtocol.js';
2
4
  // ─── Naming conventions ─────────────────────────
3
5
  /** The five a Crud handler brings answer at the collection or the row, never under their own name. */
4
6
  const CRUD = new Set(['list', 'findById', 'create', 'update', 'delete']);
@@ -7,30 +9,53 @@ function pluralize(name) {
7
9
  ? name.slice(0, -1) + 'ies'
8
10
  : name + 's';
9
11
  }
10
- /** Derive HTTP method from operation name, honoring frond.config.ts overrides. */
11
- function deriveMethod(opName, resolvedKind) {
12
- if (!resolvedKind) {
12
+ /** The verbs whose op makes the row it designates disappear — the one method still read off the name. */
13
+ const REMOVING = new Set(['delete', 'remove']);
14
+ /** Where a request puts what it carries — the path, the query string, the body. */
15
+ const CARRIED = new Set(['param', 'input', 'query']);
16
+ function changeOf({ binding = [], input }) {
17
+ const designated = new Set(binding.flatMap(({ source }) => (source.kind === 'param' && source.identifies ? [source.identifies] : [])));
18
+ if (designated.size !== 1)
19
+ return undefined;
20
+ const [entity] = designated;
21
+ const carried = binding.filter(({ source }) => CARRIED.has(source.kind) && !(source.kind === 'param' && source.identifies));
22
+ if (carried.some(({ source }) => source.kind === 'input')) {
23
+ if (!input || Card.titleOf(input) !== entity)
24
+ return undefined;
25
+ if (input.getOpts().patch)
26
+ return { kind: 'modification', fields: [] };
27
+ return input.derivation ? undefined : { kind: 'replacement', fields: [] };
28
+ }
29
+ const fields = carried.flatMap(({ source }) => (source.kind === 'param' && source.field?.entity === entity ? [source.name] : []));
30
+ return fields.length > 0 && fields.length === carried.length ? { kind: 'modification', fields } : undefined;
31
+ }
32
+ /**
33
+ * Read off the contract: a query is a GET, an op whose verb removes the row a DELETE, a change of
34
+ * one row a PATCH or a PUT — and anything else a POST, which promises nothing and so cannot lie.
35
+ */
36
+ function deriveMethod(opName, meta) {
37
+ if (!meta.kind) {
13
38
  throw new Error(`REST cannot project '${opName}' without its resolved operation kind. `
14
39
  + 'Build routes from App.operationsFor(), the EffectiveOperation table produced by core.');
15
40
  }
16
- if (resolvedKind === 'query')
41
+ if (meta.kind === 'query')
17
42
  return 'GET';
18
- if (opName.startsWith('create'))
19
- return 'POST';
20
- if (opName.startsWith('update') || opName.startsWith('edit'))
21
- return 'PUT';
22
- if (opName.startsWith('delete') || opName.startsWith('remove'))
43
+ if (REMOVING.has(wordsOf(opName)[0] ?? ''))
23
44
  return 'DELETE';
24
- return 'POST';
45
+ const change = changeOf(meta)?.kind;
46
+ if (change === 'modification')
47
+ return 'PATCH';
48
+ return change === 'replacement' ? 'PUT' : 'POST';
25
49
  }
26
50
  /**
27
51
  * Read off the signature: what designates a row comes before the op, the other required values
28
- * after it, and an optional one is left to the query string.
29
- * `publish(id: Post['id'])` → `/posts/:id/publish`, `move(id: Post['id'], position: number)` →
30
- * `/posts/:id/move/:position`, `byTag(tag: string)` → `/posts/by-tag/:tag`.
52
+ * after it, and an optional one is left to the query string — save the fields a change carries,
53
+ * which the body holds. `publish(id: Post['id'])` → `/posts/:id/publish`,
54
+ * `move(id: Post['id'], position: number)` → `/posts/:id/move/:position`,
55
+ * `retitle(id: Post['id'], title: Post['title'])` → `/posts/:id/retitle`, `byTag(tag: string)` → `/posts/by-tag/:tag`.
31
56
  */
32
- function derivePath(address, opName, binding = []) {
33
- const required = binding.flatMap(({ source, optional }) => source.kind === 'param' && !optional ? [source] : []);
57
+ function derivePath(address, opName, binding = [], inBody = []) {
58
+ const required = binding.flatMap(({ source, optional }) => source.kind === 'param' && !optional && !inBody.includes(source.name) ? [source] : []);
34
59
  const designating = required.filter((param) => param.identifies);
35
60
  const following = required.filter((param) => !param.identifies);
36
61
  const named = CRUD.has(opName) ? [] : [opName.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase())];
@@ -39,16 +64,17 @@ function derivePath(address, opName, binding = []) {
39
64
  function segmentOf({ name }) {
40
65
  return `:${name}`;
41
66
  }
42
- // ─── Route generation helpers ───────────────────
43
- // Update routes carry the SAME fields: input omissibility is a projection of the
44
- // route's MODE (operationName 'update' → patch), never forged per-field flags.
45
- // Membership is the axes-derived `Visibility.input` projection from @fougere/schema.
46
67
  // ─── Public API ─────────────────────────────────
47
68
  /** Generate REST route definitions from a fougere App — one route per operation, per address served. */
48
69
  export function generateRoutes(app, options = {}) {
49
70
  return app.fronds.flatMap((frond) => [...new Set(frond.handlers.map((handler) => handler.address))]
50
71
  .flatMap((address) => routesOf(app, frond, address, options)));
51
72
  }
73
+ /** `/api/blog` — where the app mounts REST, then the segment the frond answers under. */
74
+ function baseOf(app, frond) {
75
+ const segment = frond.protocols?.rest?.path ?? `/${frond.name}`;
76
+ return prefixOf(app.protocols) + segment;
77
+ }
52
78
  /** What one address answers on REST — nothing, when this surface does not serve it. */
53
79
  function routesOf(app, frond, address, options) {
54
80
  const surface = options.surface;
@@ -71,22 +97,19 @@ function routesOf(app, frond, address, options) {
71
97
  // facades, which intentionally cannot enumerate their keys before discovery.
72
98
  return [...effectiveOperations.keys()].map((opName) => routeFor({
73
99
  address,
74
- frond,
100
+ base: baseOf(app, frond),
75
101
  facade,
76
102
  opName,
77
103
  meta: effectiveOperations.get(opName),
78
- options,
104
+ thirdParty: app.protocols?.rest?.overrides?.[new RouteAddress({ address, operation: opName }).toString()],
79
105
  }));
80
106
  }
81
- function routeFor({ address, frond, facade, opName, meta, options }) {
107
+ function routeFor({ address, base, facade, opName, meta, thirdParty }) {
82
108
  if (!meta) {
83
109
  throw new Error(`REST facade '${address}' exposes '${opName}' but its EffectiveOperation table does not.`);
84
110
  }
85
- // What the FROND said, then what this call said, then the convention. The frond comes first
86
- // because it names the operation; a host that overrides is deciding for someone else's,
87
- // which is what `overrides:` is for and why it wins.
88
- const stated = frond.operationsOverrides?.[opName]?.rest;
89
- const override = { ...stated, ...(options.overrides?.[address] ?? {})[opName] };
111
+ // What the app said of a frond it cannot edit, then what the frond said, then the contract.
112
+ const override = { ...meta.protocols?.rest, ...thirdParty };
90
113
  // Handler and method overrides are already executed by the facade, from the same
91
114
  // EffectiveOperation local and RPC use. The adapter never resolves DI itself.
92
115
  const op = facade[opName];
@@ -94,8 +117,8 @@ function routeFor({ address, frond, facade, opName, meta, options }) {
94
117
  throw new Error(`REST EffectiveOperation table exposes '${opName}' but its facade does not.`);
95
118
  }
96
119
  return {
97
- method: override.method ?? deriveMethod(opName, meta.kind),
98
- path: (options.prefix ?? '') + (override.path ?? derivePath(address, opName, meta.binding)),
120
+ method: override.method ?? deriveMethod(opName, meta),
121
+ path: base + (override.path ?? derivePath(address, opName, meta.binding, changeOf(meta)?.fields)),
99
122
  operationName: opName,
100
123
  address,
101
124
  handler: (invocation) => op(invocation),
@@ -1 +1 @@
1
- {"version":3,"file":"RouteDefinition.js","sourceRoot":"","sources":["../../src/routes/RouteDefinition.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RouteDefinition.js","sourceRoot":"","sources":["../../src/routes/RouteDefinition.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.20.0-alpha.0",
3
+ "version": "0.21.0-alpha.0",
4
4
  "description": "Entity → REST routes.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -30,9 +30,9 @@
30
30
  "src"
31
31
  ],
32
32
  "dependencies": {
33
- "@fougere/core": "0.20.0-alpha.0",
34
- "@fougere/schema": "0.20.0-alpha.0",
35
- "@fougere/http": "0.20.0-alpha.0"
33
+ "@fougere/core": "0.21.0-alpha.0",
34
+ "@fougere/schema": "0.21.0-alpha.0",
35
+ "@fougere/http": "0.21.0-alpha.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -1,13 +1,67 @@
1
1
  import { Format, type Accepted } from '@fougere/schema';
2
- import { Protocols } from '@fougere/core';
2
+ import { HTTP_METHODS } from '@fougere/http';
3
+ import { Protocols, RouteAddress, isScanned, type FrondDescriptor, type ProtocolConfig } from '@fougere/core';
3
4
 
4
- /** What `protocols: { rest: … }` may say — nothing yet, so nothing is admitted. */
5
- const REST = Format.of('protocol/rest').closed();
5
+ /** Where one operation answers, when what its contract derives is not what it should be. */
6
+ const ROUTE = Format.of('protocol/rest/route')
7
+ .key('method', Format.tokens(HTTP_METHODS))
8
+ .key('path', Format.text)
9
+ .key('status', Format.count)
10
+ .closed();
11
+
12
+ /**
13
+ * Where the app mounts REST, and the routes of a frond it cannot edit — `betterAuth()`, a package's.
14
+ * A frond of this project says its own in its `frond.config.ts`.
15
+ */
16
+ const REST = Format.of('protocol/rest')
17
+ .key('prefix', Format.text)
18
+ .key('overrides', Format.recordOf(ROUTE))
19
+ .closed();
20
+
21
+ /** What a frond says of itself: the segment it answers under, `/journal` rather than `/blog`. */
22
+ const FROND = Format.of('protocol/rest/frond')
23
+ .key('path', Format.text)
24
+ .closed();
25
+
26
+ export type RestRoute = Accepted<typeof ROUTE>;
6
27
 
7
28
  declare module '@fougere/core' {
8
29
  interface FougereProtocols {
9
30
  rest: Accepted<typeof REST>;
10
31
  }
32
+ interface FougereOperationProtocols {
33
+ rest?: RestRoute;
34
+ }
35
+ interface FougereFrondProtocols {
36
+ rest?: Accepted<typeof FROND>;
37
+ }
38
+ }
39
+
40
+ /** Where REST is mounted when `protocols.rest.prefix` says nothing. */
41
+ export const PREFIX = '/api';
42
+
43
+ export function prefixOf(protocols: ProtocolConfig | undefined): string {
44
+ return protocols?.rest?.prefix ?? PREFIX;
45
+ }
46
+
47
+ /** An override of `fougere.config.ts` that a `frond.config.ts` should have said, or that names no op. */
48
+ function misplaced(overrides: Record<string, RestRoute> | undefined, fronds: readonly FrondDescriptor[]): string[] {
49
+ return Object.entries(overrides ?? {}).flatMap(([key, route]) => {
50
+ const named = RouteAddress.parse(key);
51
+ if (!named) return [`overrides: '${key}' names no address. An op is named '<address>.<op>'.`];
52
+
53
+ const owner = fronds.find((frond) => frond.handlers.some((handler) => handler.address === named.address));
54
+ if (!owner) return [`overrides: '${key}' — no frond serves the address '${named.address}'.`];
55
+ if (!isScanned(owner)) return [];
56
+
57
+ return [
58
+ `overrides: '${key}' belongs to ${owner.name}, whose frond.config.ts is in this project — state it there:\n`
59
+ + ` ${owner.source.path}/frond.config.ts → operations: { '${key}': { rest: ${JSON.stringify(route)} } }`,
60
+ ];
61
+ });
11
62
  }
12
63
 
13
- Protocols.register('rest', { format: REST });
64
+ Protocols.register('rest', {
65
+ format: REST,
66
+ refusals: (options: Accepted<typeof REST>, fronds) => misplaced(options.overrides, fronds),
67
+ });
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import './RestProtocol.js';
2
2
  export { generateRoutes, type RouteDefinition } from './routes/RouteDefinition.js';
3
3
  export type { HttpMethod } from '@fougere/http';
4
+ export { PREFIX, prefixOf, type RestRoute } from './RestProtocol.js';
4
5
  export { registerRoutes } from './register.js';
5
6
 
6
7
  // No schema endpoint here. Discovery is `rpc.discover` on the envelope — one
@@ -1,10 +1,4 @@
1
- import type { HttpMethod } from '@fougere/http';
2
-
3
1
  export interface GenerateRoutesOptions {
4
- /** Base path prefix (e.g. '/api'). Default: ''. */
5
- prefix?: string;
6
- /** Override route config per address, then per operation. */
7
- overrides?: Record<string, Record<string, { method?: HttpMethod; path?: string; status?: number }>>;
8
2
  /** Keep only the addresses this answers true for. */
9
3
  filter?: (address: string, frondName: string) => boolean;
10
4
  /** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
@@ -2,4 +2,4 @@ import type { EffectiveOperation } from '@fougere/core';
2
2
 
3
3
  /** What a route reads of an effective operation — its `binding` is what the path is derived from. */
4
4
  export type OperationMeta = Pick<EffectiveOperation, 'input' | 'output' | 'kind' | 'description'>
5
- & Partial<Pick<EffectiveOperation, 'binding'>>;
5
+ & Partial<Pick<EffectiveOperation, 'binding' | 'protocols'>>;
@@ -1,10 +1,11 @@
1
1
  import type { Fields } from '@fougere/schema';
2
- import { Visibility } from '@fougere/schema';
2
+ import { Card, Visibility } from '@fougere/schema';
3
3
  import type { HttpMethod } from '@fougere/http';
4
- import type { HandlerEntry as CoreHandlerEntry, OperationOverride } from '@fougere/core';
4
+ import { RouteAddress, wordsOf, type FougereFrondProtocols, type HandlerEntry as CoreHandlerEntry, type ProtocolConfig } from '@fougere/core';
5
5
  import type { BindingPlan } from '@fougere/core/descriptor';
6
6
  import type { OperationMeta } from './OperationMeta.js';
7
7
  import type { GenerateRoutesOptions } from './GenerateRoutesOptions.js';
8
+ import { prefixOf, type RestRoute } from '../RestProtocol.js';
8
9
 
9
10
  export interface RouteDefinition {
10
11
  method: HttpMethod;
@@ -47,12 +48,13 @@ interface FrondLike {
47
48
  handlers: HandlerEntry[];
48
49
  presenters: PresenterEntry[];
49
50
  surfaces?: Record<string, string[]>;
50
- /** What `frond.config.ts` said per op — `rest:` is read here, `graphql:` next facade. */
51
- operationsOverrides?: Record<string, Pick<OperationOverride, 'rest'>>;
51
+ protocols?: FougereFrondProtocols;
52
52
  }
53
53
 
54
54
  interface AppLike {
55
55
  fronds: FrondLike[];
56
+ /** What `fougere.config.ts` says to each protocol — where REST is mounted, a third party's routes. */
57
+ protocols?: ProtocolConfig;
56
58
  /** The façade an address serves to one audience — `undefined` when none. */
57
59
  facadeFor(address: string, surface?: string): Record<string, Function> | undefined;
58
60
  /** Canonical operation table produced by core. */
@@ -72,33 +74,70 @@ function pluralize(name: string): string {
72
74
  : name + 's';
73
75
  }
74
76
 
75
- /** Derive HTTP method from operation name, honoring frond.config.ts overrides. */
76
- function deriveMethod(
77
- opName: string,
78
- resolvedKind: OperationMeta['kind'],
79
- ): HttpMethod {
80
- if (!resolvedKind) {
77
+ /** The verbs whose op makes the row it designates disappear — the one method still read off the name. */
78
+ const REMOVING = new Set(['delete', 'remove']);
79
+
80
+ /** Where a request puts what it carries — the path, the query string, the body. */
81
+ const CARRIED = new Set(['param', 'input', 'query']);
82
+
83
+ /**
84
+ * What an op that designates one row does to it, read off its contract: hand the row whole — a
85
+ * replacement — or hand what changes, a `Partial<Post>` or parameters that are all its fields.
86
+ * Those `fields` travel in the body: the path designates, the body carries the change.
87
+ */
88
+ interface Change {
89
+ kind: 'replacement' | 'modification';
90
+ fields: string[];
91
+ }
92
+
93
+ function changeOf({ binding = [], input }: OperationMeta): Change | undefined {
94
+ const designated = new Set(binding.flatMap(({ source }) => (source.kind === 'param' && source.identifies ? [source.identifies] : [])));
95
+ if (designated.size !== 1) return undefined;
96
+ const [entity] = designated;
97
+
98
+ const carried = binding.filter(({ source }) => CARRIED.has(source.kind) && !(source.kind === 'param' && source.identifies));
99
+ if (carried.some(({ source }) => source.kind === 'input')) {
100
+ if (!input || Card.titleOf(input) !== entity) return undefined;
101
+ if (input.getOpts().patch) return { kind: 'modification', fields: [] };
102
+
103
+ return input.derivation ? undefined : { kind: 'replacement', fields: [] };
104
+ }
105
+
106
+ const fields = carried.flatMap(({ source }) => (source.kind === 'param' && source.field?.entity === entity ? [source.name] : []));
107
+
108
+ return fields.length > 0 && fields.length === carried.length ? { kind: 'modification', fields } : undefined;
109
+ }
110
+
111
+ /**
112
+ * Read off the contract: a query is a GET, an op whose verb removes the row a DELETE, a change of
113
+ * one row a PATCH or a PUT — and anything else a POST, which promises nothing and so cannot lie.
114
+ */
115
+ function deriveMethod(opName: string, meta: OperationMeta): HttpMethod {
116
+ if (!meta.kind) {
81
117
  throw new Error(
82
118
  `REST cannot project '${opName}' without its resolved operation kind. `
83
119
  + 'Build routes from App.operationsFor(), the EffectiveOperation table produced by core.',
84
120
  );
85
121
  }
86
- if (resolvedKind === 'query') return 'GET';
87
- if (opName.startsWith('create')) return 'POST';
88
- if (opName.startsWith('update') || opName.startsWith('edit')) return 'PUT';
89
- if (opName.startsWith('delete') || opName.startsWith('remove')) return 'DELETE';
122
+ if (meta.kind === 'query') return 'GET';
123
+ if (REMOVING.has(wordsOf(opName)[0] ?? '')) return 'DELETE';
124
+
125
+ const change = changeOf(meta)?.kind;
126
+ if (change === 'modification') return 'PATCH';
90
127
 
91
- return 'POST';
128
+ return change === 'replacement' ? 'PUT' : 'POST';
92
129
  }
93
130
 
94
131
  /**
95
132
  * Read off the signature: what designates a row comes before the op, the other required values
96
- * after it, and an optional one is left to the query string.
97
- * `publish(id: Post['id'])` → `/posts/:id/publish`, `move(id: Post['id'], position: number)` →
98
- * `/posts/:id/move/:position`, `byTag(tag: string)` → `/posts/by-tag/:tag`.
133
+ * after it, and an optional one is left to the query string — save the fields a change carries,
134
+ * which the body holds. `publish(id: Post['id'])` → `/posts/:id/publish`,
135
+ * `move(id: Post['id'], position: number)` → `/posts/:id/move/:position`,
136
+ * `retitle(id: Post['id'], title: Post['title'])` → `/posts/:id/retitle`, `byTag(tag: string)` → `/posts/by-tag/:tag`.
99
137
  */
100
- function derivePath(address: string, opName: string, binding: BindingPlan = []): string {
101
- const required = binding.flatMap(({ source, optional }) => source.kind === 'param' && !optional ? [source] : []);
138
+ function derivePath(address: string, opName: string, binding: BindingPlan = [], inBody: string[] = []): string {
139
+ const required = binding.flatMap(({ source, optional }) =>
140
+ source.kind === 'param' && !optional && !inBody.includes(source.name) ? [source] : []);
102
141
  const designating = required.filter((param) => param.identifies);
103
142
  const following = required.filter((param) => !param.identifies);
104
143
  const named = CRUD.has(opName) ? [] : [opName.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase())];
@@ -110,12 +149,6 @@ function segmentOf({ name }: { name: string }): string {
110
149
  return `:${name}`;
111
150
  }
112
151
 
113
- // ─── Route generation helpers ───────────────────
114
-
115
- // Update routes carry the SAME fields: input omissibility is a projection of the
116
- // route's MODE (operationName 'update' → patch), never forged per-field flags.
117
- // Membership is the axes-derived `Visibility.input` projection from @fougere/schema.
118
-
119
152
  // ─── Public API ─────────────────────────────────
120
153
 
121
154
  /** Generate REST route definitions from a fougere App — one route per operation, per address served. */
@@ -125,6 +158,13 @@ export function generateRoutes(app: AppLike, options: GenerateRoutesOptions = {}
125
158
  .flatMap((address) => routesOf(app, frond, address, options)));
126
159
  }
127
160
 
161
+ /** `/api/blog` — where the app mounts REST, then the segment the frond answers under. */
162
+ function baseOf(app: AppLike, frond: FrondLike): string {
163
+ const segment = frond.protocols?.rest?.path ?? `/${frond.name}`;
164
+
165
+ return prefixOf(app.protocols) + segment;
166
+ }
167
+
128
168
  /** What one address answers on REST — nothing, when this surface does not serve it. */
129
169
  function routesOf(
130
170
  app: AppLike,
@@ -152,38 +192,35 @@ function routesOf(
152
192
  // facades, which intentionally cannot enumerate their keys before discovery.
153
193
  return [...effectiveOperations.keys()].map((opName) => routeFor({
154
194
  address,
155
- frond,
195
+ base: baseOf(app, frond),
156
196
  facade,
157
197
  opName,
158
198
  meta: effectiveOperations.get(opName),
159
- options,
199
+ thirdParty: app.protocols?.rest?.overrides?.[new RouteAddress({ address, operation: opName }).toString()],
160
200
  }));
161
201
  }
162
202
 
163
203
  /** One operation at one address, and everything that decides how it is addressed. */
164
204
  interface Projecting {
165
205
  address: string;
166
- frond: FrondLike;
206
+ /** `/api/blog` — what every path of this frond starts with. */
207
+ base: string;
167
208
  facade: HandlerFacade;
168
209
  opName: string;
169
210
  meta: OperationMeta | undefined;
170
- options: GenerateRoutesOptions;
211
+ /** What `fougere.config.ts` says of this op — only ever a frond the project cannot edit. */
212
+ thirdParty: RestRoute | undefined;
171
213
  }
172
214
 
173
- function routeFor({ address, frond, facade, opName, meta, options }: Projecting): RouteDefinition {
215
+ function routeFor({ address, base, facade, opName, meta, thirdParty }: Projecting): RouteDefinition {
174
216
  if (!meta) {
175
217
  throw new Error(
176
218
  `REST facade '${address}' exposes '${opName}' but its EffectiveOperation table does not.`,
177
219
  );
178
220
  }
179
221
 
180
- // What the FROND said, then what this call said, then the convention. The frond comes first
181
- // because it names the operation; a host that overrides is deciding for someone else's,
182
- // which is what `overrides:` is for and why it wins.
183
- const stated = frond.operationsOverrides?.[opName]?.rest as
184
- { method?: HttpMethod; path?: string; status?: number } | undefined;
185
- const override = { ...stated, ...(options.overrides?.[address] ?? {})[opName] } as
186
- { method?: HttpMethod; path?: string; status?: number };
222
+ // What the app said of a frond it cannot edit, then what the frond said, then the contract.
223
+ const override: RestRoute = { ...meta.protocols?.rest, ...thirdParty };
187
224
 
188
225
  // Handler and method overrides are already executed by the facade, from the same
189
226
  // EffectiveOperation local and RPC use. The adapter never resolves DI itself.
@@ -193,8 +230,8 @@ function routeFor({ address, frond, facade, opName, meta, options }: Projecting)
193
230
  }
194
231
 
195
232
  return {
196
- method: override.method ?? deriveMethod(opName, meta.kind),
197
- path: (options.prefix ?? '') + (override.path ?? derivePath(address, opName, meta.binding)),
233
+ method: override.method ?? deriveMethod(opName, meta),
234
+ path: base + (override.path ?? derivePath(address, opName, meta.binding, changeOf(meta)?.fields)),
198
235
  operationName: opName,
199
236
  address,
200
237
  handler: (invocation) => op(invocation),