@fougere/adapter-rest 0.16.7-alpha.0 → 0.17.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/routes/GenerateRoutesOptions.d.ts +3 -4
- package/dist/routes/GenerateRoutesOptions.d.ts.map +1 -1
- package/dist/routes/RouteDefinition.d.ts +9 -17
- package/dist/routes/RouteDefinition.d.ts.map +1 -1
- package/dist/routes/RouteDefinition.js +26 -31
- package/dist/routes/RouteDefinition.js.map +1 -1
- package/package.json +4 -4
- package/src/routes/GenerateRoutesOptions.ts +3 -4
- package/src/routes/RouteDefinition.ts +37 -54
- package/dist/routes/EntityEntry.d.ts +0 -8
- package/dist/routes/EntityEntry.d.ts.map +0 -1
- package/dist/routes/EntityEntry.js +0 -2
- package/dist/routes/EntityEntry.js.map +0 -1
- package/src/routes/EntityEntry.ts +0 -8
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { HttpMethod } from '@fougere/http';
|
|
2
|
-
import type { EntityEntry } from './EntityEntry.js';
|
|
3
2
|
export interface GenerateRoutesOptions {
|
|
4
3
|
/** Base path prefix (e.g. '/api'). Default: ''. */
|
|
5
4
|
prefix?: string;
|
|
6
|
-
/** Override route config per
|
|
5
|
+
/** Override route config per address, then per operation. */
|
|
7
6
|
overrides?: Record<string, Record<string, {
|
|
8
7
|
method?: HttpMethod;
|
|
9
8
|
path?: string;
|
|
10
9
|
status?: number;
|
|
11
10
|
}>>;
|
|
12
|
-
/**
|
|
13
|
-
filter?: (
|
|
11
|
+
/** Keep only the addresses this answers true for. */
|
|
12
|
+
filter?: (address: string, frondName: string) => boolean;
|
|
14
13
|
/** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
|
|
15
14
|
surface?: string;
|
|
16
15
|
}
|
|
@@ -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;
|
|
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,14 +1,14 @@
|
|
|
1
|
-
import type { Fields
|
|
1
|
+
import type { Fields } from '@fougere/schema';
|
|
2
2
|
import type { HttpMethod } from '@fougere/http';
|
|
3
3
|
import type { HandlerEntry as CoreHandlerEntry } from '@fougere/core';
|
|
4
|
-
import type { EntityEntry } from './EntityEntry.js';
|
|
5
4
|
import type { OperationMeta } from './OperationMeta.js';
|
|
6
5
|
import type { GenerateRoutesOptions } from './GenerateRoutesOptions.js';
|
|
7
6
|
export interface RouteDefinition {
|
|
8
7
|
method: HttpMethod;
|
|
9
8
|
path: string;
|
|
10
9
|
operationName: string;
|
|
11
|
-
|
|
10
|
+
/** Where the operation answers — `checkout` in `checkout.pay`. */
|
|
11
|
+
address: string;
|
|
12
12
|
/** Handler facade method to call (receives InvocationContext). */
|
|
13
13
|
handler: (invocation: unknown) => Promise<unknown>;
|
|
14
14
|
/** Input schema (for validation / JSON schema generation). */
|
|
@@ -20,22 +20,14 @@ export interface RouteDefinition {
|
|
|
20
20
|
/** The operation in words — the method's own doc sentence, carried by the contract. */
|
|
21
21
|
description?: string;
|
|
22
22
|
}
|
|
23
|
-
/** Only what this projection reads of a scanned handler
|
|
24
|
-
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface'
|
|
25
|
-
/** `Crud(Post, PostPublic)` — the handler-wide output view, scoping every op. */
|
|
26
|
-
outputOverride?: SchemaView;
|
|
27
|
-
/** The scanned constructor, which carries the same statement made on the class. */
|
|
28
|
-
ctor?: (new (...args: never[]) => unknown) & {
|
|
29
|
-
__output?: SchemaView;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
23
|
+
/** Only what this projection reads of a scanned handler. */
|
|
24
|
+
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface' | 'exposed'>;
|
|
32
25
|
interface PresenterEntry {
|
|
33
26
|
entityName: string;
|
|
34
27
|
fields: string[];
|
|
35
28
|
}
|
|
36
29
|
interface FrondLike {
|
|
37
30
|
name: string;
|
|
38
|
-
entities: EntityEntry[];
|
|
39
31
|
handlers: HandlerEntry[];
|
|
40
32
|
presenters: PresenterEntry[];
|
|
41
33
|
surfaces?: Record<string, string[]>;
|
|
@@ -50,12 +42,12 @@ interface FrondLike {
|
|
|
50
42
|
}
|
|
51
43
|
interface AppLike {
|
|
52
44
|
fronds: FrondLike[];
|
|
53
|
-
/** The façade an
|
|
54
|
-
facadeFor(
|
|
45
|
+
/** The façade an address serves to one audience — `undefined` when none. */
|
|
46
|
+
facadeFor(address: string, surface?: string): Record<string, Function> | undefined;
|
|
55
47
|
/** Canonical operation table produced by core. */
|
|
56
|
-
operationsFor(
|
|
48
|
+
operationsFor(address: string, surface?: string): Map<string, OperationMeta> | undefined;
|
|
57
49
|
}
|
|
58
|
-
/** Generate REST route definitions from a fougere App. */
|
|
50
|
+
/** Generate REST route definitions from a fougere App — one route per operation, per address served. */
|
|
59
51
|
export declare function generateRoutes(app: AppLike, options?: GenerateRoutesOptions): RouteDefinition[];
|
|
60
52
|
export {};
|
|
61
53
|
//# sourceMappingURL=RouteDefinition.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteDefinition.d.ts","sourceRoot":"","sources":["../../src/routes/RouteDefinition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,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,KAAK,EAAE,YAAY,IAAI,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtE,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;QAAE,IAAI,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CACtG;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;AA8DD,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,qBAA0B,GAAG,eAAe,EAAE,CAInG"}
|
|
@@ -24,9 +24,9 @@ function deriveMethod(opName, resolvedKind) {
|
|
|
24
24
|
return 'DELETE';
|
|
25
25
|
return 'POST';
|
|
26
26
|
}
|
|
27
|
-
/** Derive route path from
|
|
28
|
-
function derivePath(
|
|
29
|
-
const base = `/${pluralize(
|
|
27
|
+
/** Derive route path from the address + operation name. */
|
|
28
|
+
function derivePath(address, opName) {
|
|
29
|
+
const base = `/${pluralize(address)}`;
|
|
30
30
|
// Standard CRUD
|
|
31
31
|
if (opName === 'list')
|
|
32
32
|
return base;
|
|
@@ -49,52 +49,49 @@ function derivePath(entityName, opName) {
|
|
|
49
49
|
// route's MODE (operationName 'update' → patch), never forged per-field flags.
|
|
50
50
|
// Membership is the axes-derived `Visibility.input` projection from @fougere/schema.
|
|
51
51
|
// ─── Public API ─────────────────────────────────
|
|
52
|
-
/** Generate REST route definitions from a fougere App. */
|
|
52
|
+
/** Generate REST route definitions from a fougere App — one route per operation, per address served. */
|
|
53
53
|
export function generateRoutes(app, options = {}) {
|
|
54
|
-
return app.fronds.flatMap((frond) => frond.
|
|
54
|
+
return app.fronds.flatMap((frond) => [...new Set(frond.handlers.map((handler) => handler.address))]
|
|
55
|
+
.flatMap((address) => routesOf(app, frond, address, options)));
|
|
55
56
|
}
|
|
56
|
-
/** What one
|
|
57
|
-
function routesOf(app, frond,
|
|
57
|
+
/** What one address answers on REST — nothing, when this surface does not serve it. */
|
|
58
|
+
function routesOf(app, frond, address, options) {
|
|
58
59
|
const surface = options.surface;
|
|
59
60
|
// Membership is core's answer, not ours — one rule, read here (see App.facadeFor).
|
|
60
|
-
const facade = app.facadeFor(
|
|
61
|
+
const facade = app.facadeFor(address, surface);
|
|
61
62
|
if (!facade)
|
|
62
63
|
return [];
|
|
63
|
-
if (options.filter && !options.filter(
|
|
64
|
+
if (options.filter && !options.filter(address, frond.name))
|
|
64
65
|
return [];
|
|
65
|
-
|
|
66
|
+
const handler = (surface
|
|
67
|
+
? frond.handlers.find((one) => one.address === address && one.surface === surface)
|
|
68
|
+
: undefined) ?? frond.handlers.find((one) => !one.surface && one.address === address);
|
|
69
|
+
if (!surface && handler?.exposed === false)
|
|
66
70
|
return [];
|
|
67
|
-
const effectiveOperations = app.operationsFor(
|
|
71
|
+
const effectiveOperations = app.operationsFor(address, surface);
|
|
68
72
|
if (!effectiveOperations) {
|
|
69
|
-
throw new Error(`REST cannot project '${
|
|
73
|
+
throw new Error(`REST cannot project '${address}' without its EffectiveOperation table.`);
|
|
70
74
|
}
|
|
71
|
-
const handler = (surface
|
|
72
|
-
? frond.handlers.find((one) => one.address === entity.name && one.surface === surface)
|
|
73
|
-
: undefined) ?? frond.handlers.find((one) => !one.surface && one.address === entity.name);
|
|
74
|
-
// The handler's output schema if declared, otherwise the entity. A frond whose class never
|
|
75
|
-
// crossed the wire arrives as one too: boot rebuilds the card before here.
|
|
76
|
-
const outputSchema = handler?.outputOverride ?? handler?.ctor?.__output ?? entity.entityClass;
|
|
77
75
|
// The resolved table defines the public operation set. This also works for remote proxy
|
|
78
76
|
// facades, which intentionally cannot enumerate their keys before discovery.
|
|
79
77
|
return [...effectiveOperations.keys()].map((opName) => routeFor({
|
|
80
|
-
|
|
78
|
+
address,
|
|
81
79
|
frond,
|
|
82
80
|
facade,
|
|
83
|
-
fields: outputSchema.getFields(),
|
|
84
81
|
opName,
|
|
85
82
|
meta: effectiveOperations.get(opName),
|
|
86
83
|
options,
|
|
87
84
|
}));
|
|
88
85
|
}
|
|
89
|
-
function routeFor({
|
|
86
|
+
function routeFor({ address, frond, facade, opName, meta, options }) {
|
|
90
87
|
if (!meta) {
|
|
91
|
-
throw new Error(`REST facade '${
|
|
88
|
+
throw new Error(`REST facade '${address}' exposes '${opName}' but its EffectiveOperation table does not.`);
|
|
92
89
|
}
|
|
93
90
|
// What the FROND said, then what this call said, then the convention. The frond comes first
|
|
94
91
|
// because it names the operation; a host that overrides is deciding for someone else's,
|
|
95
92
|
// which is what `overrides:` is for and why it wins.
|
|
96
93
|
const stated = frond.operationsOverrides?.[opName]?.rest;
|
|
97
|
-
const override = { ...stated, ...(options.overrides?.[
|
|
94
|
+
const override = { ...stated, ...(options.overrides?.[address] ?? {})[opName] };
|
|
98
95
|
// Handler and method overrides are already executed by the facade, from the same
|
|
99
96
|
// EffectiveOperation local and RPC use. The adapter never resolves DI itself.
|
|
100
97
|
const op = facade[opName];
|
|
@@ -103,22 +100,20 @@ function routeFor({ entity, frond, facade, fields, opName, meta, options }) {
|
|
|
103
100
|
}
|
|
104
101
|
return {
|
|
105
102
|
method: override.method ?? deriveMethod(opName, meta.kind),
|
|
106
|
-
path: (options.prefix ?? '') + (override.path ?? derivePath(
|
|
103
|
+
path: (options.prefix ?? '') + (override.path ?? derivePath(address, opName)),
|
|
107
104
|
operationName: opName,
|
|
108
|
-
|
|
105
|
+
address,
|
|
109
106
|
handler: (invocation) => op(invocation),
|
|
110
|
-
...inputAndOutput(meta
|
|
107
|
+
...inputAndOutput(meta),
|
|
111
108
|
successStatus: override.status,
|
|
112
109
|
...(meta.description && { description: meta.description }),
|
|
113
110
|
};
|
|
114
111
|
}
|
|
115
112
|
/** Both pass through the client-surface projections — write-only out, read-only in. */
|
|
116
|
-
function inputAndOutput(meta
|
|
117
|
-
const inputFields = meta.input?.getFields()
|
|
118
|
-
?? (opName === 'create' || opName === 'update' ? Visibility.of(fields).input : undefined);
|
|
113
|
+
function inputAndOutput(meta) {
|
|
119
114
|
return {
|
|
120
|
-
inputFields,
|
|
121
|
-
outputFields: meta.output ? Visibility.of(meta.output.getFields()).output :
|
|
115
|
+
inputFields: meta.input ? Visibility.of(meta.input.getFields()).input : undefined,
|
|
116
|
+
outputFields: meta.output ? Visibility.of(meta.output.getFields()).output : undefined,
|
|
122
117
|
};
|
|
123
118
|
}
|
|
124
119
|
//# sourceMappingURL=RouteDefinition.js.map
|
|
@@ -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.17.0-alpha.0",
|
|
4
4
|
"description": "Entity → REST routes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@fougere/core": "0.
|
|
33
|
-
"@fougere/schema": "0.
|
|
34
|
-
"@fougere/http": "0.
|
|
32
|
+
"@fougere/core": "0.17.0-alpha.0",
|
|
33
|
+
"@fougere/schema": "0.17.0-alpha.0",
|
|
34
|
+
"@fougere/http": "0.17.0-alpha.0"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { HttpMethod } from '@fougere/http';
|
|
2
|
-
import type { EntityEntry } from './EntityEntry.js';
|
|
3
2
|
|
|
4
3
|
export interface GenerateRoutesOptions {
|
|
5
4
|
/** Base path prefix (e.g. '/api'). Default: ''. */
|
|
6
5
|
prefix?: string;
|
|
7
|
-
/** Override route config per
|
|
6
|
+
/** Override route config per address, then per operation. */
|
|
8
7
|
overrides?: Record<string, Record<string, { method?: HttpMethod; path?: string; status?: number }>>;
|
|
9
|
-
/**
|
|
10
|
-
filter?: (
|
|
8
|
+
/** Keep only the addresses this answers true for. */
|
|
9
|
+
filter?: (address: string, frondName: string) => boolean;
|
|
11
10
|
/** Surface name for filtering (e.g. 'rest', 'graphql'). Uses frond.config.ts surfaces if set. */
|
|
12
11
|
surface?: string;
|
|
13
12
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { Fields
|
|
1
|
+
import type { Fields } from '@fougere/schema';
|
|
2
2
|
import { Visibility } from '@fougere/schema';
|
|
3
3
|
import type { HttpMethod } from '@fougere/http';
|
|
4
4
|
import type { HandlerEntry as CoreHandlerEntry } from '@fougere/core';
|
|
5
|
-
import type { EntityEntry } from './EntityEntry.js';
|
|
6
5
|
import type { OperationMeta } from './OperationMeta.js';
|
|
7
6
|
import type { GenerateRoutesOptions } from './GenerateRoutesOptions.js';
|
|
8
7
|
|
|
@@ -10,7 +9,8 @@ export interface RouteDefinition {
|
|
|
10
9
|
method: HttpMethod;
|
|
11
10
|
path: string;
|
|
12
11
|
operationName: string;
|
|
13
|
-
|
|
12
|
+
/** Where the operation answers — `checkout` in `checkout.pay`. */
|
|
13
|
+
address: string;
|
|
14
14
|
/** Handler facade method to call (receives InvocationContext). */
|
|
15
15
|
handler: (invocation: unknown) => Promise<unknown>;
|
|
16
16
|
/** Input schema (for validation / JSON schema generation). */
|
|
@@ -26,13 +26,8 @@ export interface RouteDefinition {
|
|
|
26
26
|
// (`PresenterExecutor`), so the rows arrive computed and a second pass was duplicated work.
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** Only what this projection reads of a scanned handler
|
|
30
|
-
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface'
|
|
31
|
-
/** `Crud(Post, PostPublic)` — the handler-wide output view, scoping every op. */
|
|
32
|
-
outputOverride?: SchemaView;
|
|
33
|
-
/** The scanned constructor, which carries the same statement made on the class. */
|
|
34
|
-
ctor?: (new (...args: never[]) => unknown) & { __output?: SchemaView };
|
|
35
|
-
};
|
|
29
|
+
/** Only what this projection reads of a scanned handler. */
|
|
30
|
+
type HandlerEntry = Pick<CoreHandlerEntry, 'address' | 'surface' | 'exposed'>;
|
|
36
31
|
// No `operations` here, and its absence is the point. It was declared, never read — this
|
|
37
32
|
// file takes its table from `app.operationsFor()` — and it carried `OperationMeta`, whose
|
|
38
33
|
// `kind` is REQUIRED because that is true of an EffectiveOperation. A scanned handler's
|
|
@@ -48,7 +43,6 @@ interface PresenterEntry {
|
|
|
48
43
|
|
|
49
44
|
interface FrondLike {
|
|
50
45
|
name: string;
|
|
51
|
-
entities: EntityEntry[];
|
|
52
46
|
handlers: HandlerEntry[];
|
|
53
47
|
presenters: PresenterEntry[];
|
|
54
48
|
surfaces?: Record<string, string[]>;
|
|
@@ -58,10 +52,10 @@ interface FrondLike {
|
|
|
58
52
|
|
|
59
53
|
interface AppLike {
|
|
60
54
|
fronds: FrondLike[];
|
|
61
|
-
/** The façade an
|
|
62
|
-
facadeFor(
|
|
55
|
+
/** The façade an address serves to one audience — `undefined` when none. */
|
|
56
|
+
facadeFor(address: string, surface?: string): Record<string, Function> | undefined;
|
|
63
57
|
/** Canonical operation table produced by core. */
|
|
64
|
-
operationsFor(
|
|
58
|
+
operationsFor(address: string, surface?: string): Map<string, OperationMeta> | undefined;
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
type HandlerFacade = Record<string, Function>;
|
|
@@ -97,9 +91,9 @@ function deriveMethod(
|
|
|
97
91
|
return 'POST';
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
/** Derive route path from
|
|
101
|
-
function derivePath(
|
|
102
|
-
const base = `/${pluralize(
|
|
94
|
+
/** Derive route path from the address + operation name. */
|
|
95
|
+
function derivePath(address: string, opName: string): string {
|
|
96
|
+
const base = `/${pluralize(address)}`;
|
|
103
97
|
|
|
104
98
|
// Standard CRUD
|
|
105
99
|
if (opName === 'list') return base;
|
|
@@ -124,66 +118,62 @@ function derivePath(entityName: string, opName: string): string {
|
|
|
124
118
|
|
|
125
119
|
// ─── Public API ─────────────────────────────────
|
|
126
120
|
|
|
127
|
-
/** Generate REST route definitions from a fougere App. */
|
|
121
|
+
/** Generate REST route definitions from a fougere App — one route per operation, per address served. */
|
|
128
122
|
export function generateRoutes(app: AppLike, options: GenerateRoutesOptions = {}): RouteDefinition[] {
|
|
129
123
|
return app.fronds.flatMap((frond) =>
|
|
130
|
-
frond.
|
|
124
|
+
[...new Set(frond.handlers.map((handler) => handler.address))]
|
|
125
|
+
.flatMap((address) => routesOf(app, frond, address, options)));
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
/** What one
|
|
128
|
+
/** What one address answers on REST — nothing, when this surface does not serve it. */
|
|
134
129
|
function routesOf(
|
|
135
130
|
app: AppLike,
|
|
136
131
|
frond: FrondLike,
|
|
137
|
-
|
|
132
|
+
address: string,
|
|
138
133
|
options: GenerateRoutesOptions,
|
|
139
134
|
): RouteDefinition[] {
|
|
140
135
|
const surface = options.surface;
|
|
141
136
|
// Membership is core's answer, not ours — one rule, read here (see App.facadeFor).
|
|
142
|
-
const facade = app.facadeFor(
|
|
137
|
+
const facade = app.facadeFor(address, surface) as HandlerFacade | undefined;
|
|
143
138
|
if (!facade) return [];
|
|
144
|
-
if (options.filter && !options.filter(
|
|
145
|
-
|
|
139
|
+
if (options.filter && !options.filter(address, frond.name)) return [];
|
|
140
|
+
|
|
141
|
+
const handler = (surface
|
|
142
|
+
? frond.handlers.find((one) => one.address === address && one.surface === surface)
|
|
143
|
+
: undefined) ?? frond.handlers.find((one) => !one.surface && one.address === address);
|
|
144
|
+
if (!surface && handler?.exposed === false) return [];
|
|
146
145
|
|
|
147
|
-
const effectiveOperations = app.operationsFor(
|
|
146
|
+
const effectiveOperations = app.operationsFor(address, surface);
|
|
148
147
|
if (!effectiveOperations) {
|
|
149
|
-
throw new Error(`REST cannot project '${
|
|
148
|
+
throw new Error(`REST cannot project '${address}' without its EffectiveOperation table.`);
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
const handler = (surface
|
|
153
|
-
? frond.handlers.find((one) => one.address === entity.name && one.surface === surface)
|
|
154
|
-
: undefined) ?? frond.handlers.find((one) => !one.surface && one.address === entity.name);
|
|
155
|
-
// The handler's output schema if declared, otherwise the entity. A frond whose class never
|
|
156
|
-
// crossed the wire arrives as one too: boot rebuilds the card before here.
|
|
157
|
-
const outputSchema: SchemaView = handler?.outputOverride ?? handler?.ctor?.__output ?? entity.entityClass;
|
|
158
|
-
|
|
159
151
|
// The resolved table defines the public operation set. This also works for remote proxy
|
|
160
152
|
// facades, which intentionally cannot enumerate their keys before discovery.
|
|
161
153
|
return [...effectiveOperations.keys()].map((opName) => routeFor({
|
|
162
|
-
|
|
154
|
+
address,
|
|
163
155
|
frond,
|
|
164
156
|
facade,
|
|
165
|
-
fields: outputSchema.getFields(),
|
|
166
157
|
opName,
|
|
167
158
|
meta: effectiveOperations.get(opName),
|
|
168
159
|
options,
|
|
169
160
|
}));
|
|
170
161
|
}
|
|
171
162
|
|
|
172
|
-
/** One operation
|
|
163
|
+
/** One operation at one address, and everything that decides how it is addressed. */
|
|
173
164
|
interface Projecting {
|
|
174
|
-
|
|
165
|
+
address: string;
|
|
175
166
|
frond: FrondLike;
|
|
176
167
|
facade: HandlerFacade;
|
|
177
|
-
fields: Fields;
|
|
178
168
|
opName: string;
|
|
179
169
|
meta: OperationMeta | undefined;
|
|
180
170
|
options: GenerateRoutesOptions;
|
|
181
171
|
}
|
|
182
172
|
|
|
183
|
-
function routeFor({
|
|
173
|
+
function routeFor({ address, frond, facade, opName, meta, options }: Projecting): RouteDefinition {
|
|
184
174
|
if (!meta) {
|
|
185
175
|
throw new Error(
|
|
186
|
-
`REST facade '${
|
|
176
|
+
`REST facade '${address}' exposes '${opName}' but its EffectiveOperation table does not.`,
|
|
187
177
|
);
|
|
188
178
|
}
|
|
189
179
|
|
|
@@ -192,7 +182,7 @@ function routeFor({ entity, frond, facade, fields, opName, meta, options }: Proj
|
|
|
192
182
|
// which is what `overrides:` is for and why it wins.
|
|
193
183
|
const stated = frond.operationsOverrides?.[opName]?.rest as
|
|
194
184
|
{ method?: HttpMethod; path?: string; status?: number } | undefined;
|
|
195
|
-
const override = { ...stated, ...(options.overrides?.[
|
|
185
|
+
const override = { ...stated, ...(options.overrides?.[address] ?? {})[opName] } as
|
|
196
186
|
{ method?: HttpMethod; path?: string; status?: number };
|
|
197
187
|
|
|
198
188
|
// Handler and method overrides are already executed by the facade, from the same
|
|
@@ -204,27 +194,20 @@ function routeFor({ entity, frond, facade, fields, opName, meta, options }: Proj
|
|
|
204
194
|
|
|
205
195
|
return {
|
|
206
196
|
method: override.method ?? deriveMethod(opName, meta.kind),
|
|
207
|
-
path: (options.prefix ?? '') + (override.path ?? derivePath(
|
|
197
|
+
path: (options.prefix ?? '') + (override.path ?? derivePath(address, opName)),
|
|
208
198
|
operationName: opName,
|
|
209
|
-
|
|
199
|
+
address,
|
|
210
200
|
handler: (invocation) => op(invocation),
|
|
211
|
-
...inputAndOutput(meta
|
|
201
|
+
...inputAndOutput(meta),
|
|
212
202
|
successStatus: override.status,
|
|
213
203
|
...(meta.description && { description: meta.description }),
|
|
214
204
|
};
|
|
215
205
|
}
|
|
216
206
|
|
|
217
207
|
/** Both pass through the client-surface projections — write-only out, read-only in. */
|
|
218
|
-
function inputAndOutput(
|
|
219
|
-
meta: OperationMeta,
|
|
220
|
-
fields: Fields,
|
|
221
|
-
opName: string,
|
|
222
|
-
): { inputFields: Fields | undefined; outputFields: Fields | undefined } {
|
|
223
|
-
const inputFields = meta.input?.getFields()
|
|
224
|
-
?? (opName === 'create' || opName === 'update' ? Visibility.of(fields).input : undefined);
|
|
225
|
-
|
|
208
|
+
function inputAndOutput(meta: OperationMeta): { inputFields: Fields | undefined; outputFields: Fields | undefined } {
|
|
226
209
|
return {
|
|
227
|
-
inputFields,
|
|
228
|
-
outputFields: meta.output ? Visibility.of(meta.output.getFields()).output :
|
|
210
|
+
inputFields: meta.input ? Visibility.of(meta.input.getFields()).input : undefined,
|
|
211
|
+
outputFields: meta.output ? Visibility.of(meta.output.getFields()).output : undefined,
|
|
229
212
|
};
|
|
230
213
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { SchemaView } from '@fougere/schema';
|
|
2
|
-
export interface EntityEntry {
|
|
3
|
-
name: string;
|
|
4
|
-
/** A live class in-process, a card from a frond whose class never crossed. */
|
|
5
|
-
entityClass: SchemaView;
|
|
6
|
-
exposed?: boolean;
|
|
7
|
-
}
|
|
8
|
-
//# sourceMappingURL=EntityEntry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EntityEntry.d.ts","sourceRoot":"","sources":["../../src/routes/EntityEntry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,WAAW,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EntityEntry.js","sourceRoot":"","sources":["../../src/routes/EntityEntry.ts"],"names":[],"mappings":""}
|