@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.
- package/dist/RestProtocol.d.ts +33 -2
- package/dist/RestProtocol.d.ts.map +1 -1
- package/dist/RestProtocol.js +46 -4
- package/dist/RestProtocol.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/GenerateRoutesOptions.d.ts +0 -9
- package/dist/routes/GenerateRoutesOptions.d.ts.map +1 -1
- package/dist/routes/OperationMeta.d.ts +1 -1
- package/dist/routes/OperationMeta.d.ts.map +1 -1
- package/dist/routes/RouteDefinition.d.ts +4 -3
- package/dist/routes/RouteDefinition.d.ts.map +1 -1
- package/dist/routes/RouteDefinition.js +53 -30
- package/dist/routes/RouteDefinition.js.map +1 -1
- package/package.json +4 -4
- package/src/RestProtocol.ts +58 -4
- package/src/index.ts +1 -0
- package/src/routes/GenerateRoutesOptions.ts +0 -6
- package/src/routes/OperationMeta.ts +1 -1
- package/src/routes/RouteDefinition.ts +77 -40
package/dist/RestProtocol.d.ts
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { Format, type Accepted } from '@fougere/schema';
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
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"}
|
package/dist/RestProtocol.js
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import { Format } from '@fougere/schema';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
package/dist/RestProtocol.js.map
CHANGED
|
@@ -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;
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
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 (
|
|
41
|
+
if (meta.kind === 'query')
|
|
17
42
|
return 'GET';
|
|
18
|
-
if (
|
|
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
|
-
|
|
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`,
|
|
30
|
-
*
|
|
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
|
-
|
|
104
|
+
thirdParty: app.protocols?.rest?.overrides?.[new RouteAddress({ address, operation: opName }).toString()],
|
|
79
105
|
}));
|
|
80
106
|
}
|
|
81
|
-
function routeFor({ address,
|
|
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
|
|
86
|
-
|
|
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
|
|
98
|
-
path:
|
|
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":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
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.
|
|
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.
|
|
34
|
-
"@fougere/schema": "0.
|
|
35
|
-
"@fougere/http": "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"
|
package/src/RestProtocol.ts
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
import { Format, type Accepted } from '@fougere/schema';
|
|
2
|
-
import {
|
|
2
|
+
import { HTTP_METHODS } from '@fougere/http';
|
|
3
|
+
import { Protocols, RouteAddress, isScanned, type FrondDescriptor, type ProtocolConfig } from '@fougere/core';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
const
|
|
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', {
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 (
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
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`,
|
|
98
|
-
*
|
|
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 }) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
181
|
-
|
|
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
|
|
197
|
-
path:
|
|
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),
|