@fougere/admin 0.5.0-alpha.1 → 0.7.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/extensions.d.ts.map +1 -1
- package/dist/extensions.js.map +1 -1
- package/dist/facets.d.ts +2 -26
- package/dist/facets.d.ts.map +1 -1
- package/dist/facets.js.map +1 -1
- package/dist/index.d.ts +1 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -12
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +21 -76
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +25 -82
- package/dist/provider.js.map +1 -1
- package/dist/react.js +2 -2
- package/dist/resources.d.ts +2 -15
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +3 -26
- package/dist/resources.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -10
- package/dist/runtime.js.map +1 -1
- package/dist/topology.d.ts +4 -32
- package/dist/topology.d.ts.map +1 -1
- package/dist/topology.js +4 -32
- package/dist/topology.js.map +1 -1
- package/package.json +4 -4
- package/src/extensions.ts +1 -15
- package/src/facets.ts +2 -26
- package/src/index.ts +1 -12
- package/src/provider.ts +36 -98
- package/src/react.tsx +2 -2
- package/src/resources.ts +3 -26
- package/src/runtime.ts +1 -10
- package/src/topology.ts +4 -32
package/dist/resources.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the app hosts, read as a back-office.
|
|
3
|
-
*
|
|
4
|
-
* The card (`rpc.discover`) already answers every question a back-office asks: which
|
|
5
|
-
* doors exist, what each one stores, and which of the five verbs it serves. So there
|
|
6
|
-
* is nothing to generate and nothing to configure — a new entity appears in the menu
|
|
7
|
-
* because the card grew a door, not because a file was written.
|
|
8
|
-
*
|
|
9
|
-
* Two projections of the same shape, and they are the pair `form.ts` already draws:
|
|
10
|
-
* `tableColumnsOf` for what a list SHOWS, `formFieldsOf` for what a form SUPPLIES.
|
|
11
|
-
*/
|
|
1
|
+
/** What the app hosts, read as a back-office. */
|
|
12
2
|
import { Card, FieldSet } from '@fougere/schema';
|
|
13
3
|
import { CALL_ENDPOINT, fetcher as browserFetcher, formFieldsOf, sendCall, tableColumnsOf, } from '@fougere/app/client';
|
|
14
4
|
import { EMPTY_INVOCATION } from '@fougere/core/contract';
|
|
@@ -36,13 +26,7 @@ export function capabilitiesOf(operations) {
|
|
|
36
26
|
delete: serves.has('delete'),
|
|
37
27
|
};
|
|
38
28
|
}
|
|
39
|
-
/**
|
|
40
|
-
* A door with no schema is not a resource.
|
|
41
|
-
*
|
|
42
|
-
* The card omits `schema` when nothing is stored under that name, and that is
|
|
43
|
-
* ordinary — a health check, a search across several shapes. There is no table to
|
|
44
|
-
* draw for it, and no form. It stays reachable by hand; it is simply not furniture.
|
|
45
|
-
*/
|
|
29
|
+
/** A door with no schema is not a resource. */
|
|
46
30
|
export function resourcesOf(card) {
|
|
47
31
|
const out = [];
|
|
48
32
|
for (const frond of card.fronds) {
|
|
@@ -78,14 +62,7 @@ export function resourcesOf(card) {
|
|
|
78
62
|
export function keysOf(resources) {
|
|
79
63
|
return Object.fromEntries(resources.map((r) => [r.name, { name: r.name, primary: r.primary }]));
|
|
80
64
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Ask the running app what it hosts.
|
|
83
|
-
*
|
|
84
|
-
* `rpc.discover` travels on the same wire as every other call — it is a reserved op,
|
|
85
|
-
* not a second endpoint — so a back-office needs no configuration to find its
|
|
86
|
-
* subject, and gets the same answer whether the frond is in this process or behind
|
|
87
|
-
* an address in `remotes:`.
|
|
88
|
-
*/
|
|
65
|
+
/** Ask the running app what it hosts. */
|
|
89
66
|
export async function fetchCard(endpoint = CALL_ENDPOINT, fetcher = browserFetcher) {
|
|
90
67
|
return await sendCall(fetcher, { entity: 'rpc', op: 'discover' }, EMPTY_INVOCATION, endpoint);
|
|
91
68
|
}
|
package/dist/resources.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,EAGL,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B,oFAAoF;IACpF,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,EAGL,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B,oFAAoF;IACpF,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CA0BlF"}
|
package/dist/runtime.js
CHANGED
|
@@ -6,16 +6,7 @@ import { fetchCard, keysOf, resourcesOf } from './resources.js';
|
|
|
6
6
|
export function createAdminRuntime(options = {}) {
|
|
7
7
|
const { endpoint = CALL_ENDPOINT, fetcher = browserFetcher, extensions = [], } = options;
|
|
8
8
|
let loading;
|
|
9
|
-
/**
|
|
10
|
-
* One card request, shared — and a REFUSED one is forgotten.
|
|
11
|
-
*
|
|
12
|
-
* `loading ??=` alone memoizes the rejection too, so a receiver that was down when the
|
|
13
|
-
* panel opened stays down for the life of the tab: every later attempt awaits the same
|
|
14
|
-
* dead promise and there is no path back. Clearing the slot on failure is what makes a
|
|
15
|
-
* retry mean anything, and the rejection is re-thrown so the caller can say so — a
|
|
16
|
-
* discovery that fails silently leaves react-admin on its loading page forever, which
|
|
17
|
-
* is the one state where the panel tells the operator nothing at all.
|
|
18
|
-
*/
|
|
9
|
+
/** One card request, shared — and a REFUSED one is forgotten. */
|
|
19
10
|
const load = () => loading ??= fetchCard(endpoint, fetcher)
|
|
20
11
|
.then((card) => {
|
|
21
12
|
const resources = applyAdminExtensions(resourcesOf(card), extensions);
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,EACL,aAAa,EACb,OAAO,IAAI,cAAc,GAE1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAuB,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GAEvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAsB,MAAM,gBAAgB,CAAC;AAoBpF,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAwB,EAAE;IAClE,MAAM,EACJ,QAAQ,GAAG,aAAa,EACxB,OAAO,GAAG,cAAc,EACxB,UAAU,GAAG,EAAE,GAChB,GAAG,OAAO,CAAC;IACZ,IAAI,OAAyC,CAAC;IAE9C
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,EACL,aAAa,EACb,OAAO,IAAI,cAAc,GAE1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAuB,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GAEvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAsB,MAAM,gBAAgB,CAAC;AAoBpF,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAwB,EAAE;IAClE,MAAM,EACJ,QAAQ,GAAG,aAAa,EACxB,OAAO,GAAG,cAAc,EACxB,UAAU,GAAG,EAAE,GAChB,GAAG,OAAO,CAAC;IACZ,IAAI,OAAyC,CAAC;IAE9C,iEAAiE;IACjE,MAAM,IAAI,GAAG,GAAyB,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC;SAC9E,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,MAAM,SAAS,GAAG,oBAAoB,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;QACtE,OAAO;YACL,SAAS;YACT,QAAQ,EAAE,kBAAkB,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;SAClF,CAAC;IACJ,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACxB,OAAO,GAAG,SAAS,CAAC;QACpB,MAAM,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;IAEL,OAAO;QACL,IAAI;QACJ,YAAY,EAAE,sBAAsB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC;KAC1E,CAAC;AACJ,CAAC"}
|
package/dist/topology.d.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How the system is arranged, as the app itself reports it.
|
|
3
|
-
*
|
|
4
|
-
* The card answers what a process HOSTS; this answers what SHAPE it is in — which fronds
|
|
5
|
-
* run here, which answered from somewhere else, and who called whom. It is served by
|
|
6
|
-
* `@fougere/observability` on `rpc.topology`, so absence is an ordinary answer: an app
|
|
7
|
-
* that never installed the package has no topology to publish, and says so by refusing
|
|
8
|
-
* the op. Nothing here is declared — a frond is remote because it answered a call nobody
|
|
9
|
-
* hosts, never because a config line said so.
|
|
10
|
-
*/
|
|
1
|
+
/** How the system is arranged, as the app itself reports it. */
|
|
11
2
|
import { type Fetcher } from '@fougere/app/client';
|
|
12
3
|
import { type Edge, type FrondPlacement, type TopologyReport } from '@fougere/core/contract';
|
|
13
4
|
export type { TopologyReport, FrondPlacement, Edge };
|
|
14
|
-
/**
|
|
15
|
-
* The report, or `undefined` when the app serves no topology at all.
|
|
16
|
-
*
|
|
17
|
-
* The refusal is DISTINGUISHED, not swallowed: a `NOT_FOUND` on the op is the package not
|
|
18
|
-
* being wired — something the panel can explain — while anything else is a real failure
|
|
19
|
-
* and stays a failure. One reserved op that is absent must not read as an app that is down.
|
|
20
|
-
*/
|
|
5
|
+
/** The report, or `undefined` when the app serves no topology at all. */
|
|
21
6
|
export declare function fetchTopology(endpoint?: string, fetcher?: Fetcher): Promise<TopologyReport | undefined>;
|
|
22
7
|
/** One frond as the page draws it: its placement, and the calls observed around it. */
|
|
23
8
|
export interface TopologyNode extends FrondPlacement {
|
|
@@ -26,21 +11,8 @@ export interface TopologyNode extends FrondPlacement {
|
|
|
26
11
|
/** Fronds that called this one. */
|
|
27
12
|
calledBy: Edge[];
|
|
28
13
|
}
|
|
29
|
-
/**
|
|
30
|
-
* The report read as a graph, local fronds first.
|
|
31
|
-
*
|
|
32
|
-
* Two passes over `edges` rather than one index per direction: a topology is bounded by
|
|
33
|
-
* fronds², so the whole thing is smaller than one page of rows and an index would be a
|
|
34
|
-
* second structure to keep true.
|
|
35
|
-
*/
|
|
14
|
+
/** The report read as a graph, local fronds first. */
|
|
36
15
|
export declare function nodesOf(report: TopologyReport): TopologyNode[];
|
|
37
|
-
/**
|
|
38
|
-
* A frond this process called that publishes nothing of its own shape.
|
|
39
|
-
*
|
|
40
|
-
* The honest reading of `entities: 0, doors: 0` on a remote: its shape is published by the
|
|
41
|
-
* process that owns it, under its own service name — so a panel pointed here can say the
|
|
42
|
-
* frond is reachable and cannot say what it holds. Worth naming rather than drawing as an
|
|
43
|
-
* empty frond, which reads as a frond with nothing in it.
|
|
44
|
-
*/
|
|
16
|
+
/** A frond this process called that publishes nothing of its own shape. */
|
|
45
17
|
export declare function isOpaque(node: FrondPlacement): boolean;
|
|
46
18
|
//# sourceMappingURL=topology.d.ts.map
|
package/dist/topology.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;AAErD,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,QAAQ,SAAgB,EACxB,OAAO,GAAE,OAAwB,GAChC,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAYrC;AAED,uFAAuF;AACvF,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,sDAAsD;IACtD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,IAAI,EAAE,CAAC;CAClB;AAED,sDAAsD;AACtD,wBAAgB,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAS9D;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAEtD"}
|
package/dist/topology.js
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How the system is arranged, as the app itself reports it.
|
|
3
|
-
*
|
|
4
|
-
* The card answers what a process HOSTS; this answers what SHAPE it is in — which fronds
|
|
5
|
-
* run here, which answered from somewhere else, and who called whom. It is served by
|
|
6
|
-
* `@fougere/observability` on `rpc.topology`, so absence is an ordinary answer: an app
|
|
7
|
-
* that never installed the package has no topology to publish, and says so by refusing
|
|
8
|
-
* the op. Nothing here is declared — a frond is remote because it answered a call nobody
|
|
9
|
-
* hosts, never because a config line said so.
|
|
10
|
-
*/
|
|
1
|
+
/** How the system is arranged, as the app itself reports it. */
|
|
11
2
|
import { CALL_ENDPOINT, fetcher as browserFetcher, sendCall, } from '@fougere/app/client';
|
|
12
3
|
import { EMPTY_INVOCATION, ErrorCode, } from '@fougere/core/contract';
|
|
13
|
-
/**
|
|
14
|
-
* The report, or `undefined` when the app serves no topology at all.
|
|
15
|
-
*
|
|
16
|
-
* The refusal is DISTINGUISHED, not swallowed: a `NOT_FOUND` on the op is the package not
|
|
17
|
-
* being wired — something the panel can explain — while anything else is a real failure
|
|
18
|
-
* and stays a failure. One reserved op that is absent must not read as an app that is down.
|
|
19
|
-
*/
|
|
4
|
+
/** The report, or `undefined` when the app serves no topology at all. */
|
|
20
5
|
export async function fetchTopology(endpoint = CALL_ENDPOINT, fetcher = browserFetcher) {
|
|
21
6
|
try {
|
|
22
7
|
return await sendCall(fetcher, { entity: 'rpc', op: 'topology' }, EMPTY_INVOCATION, endpoint);
|
|
@@ -27,13 +12,7 @@ export async function fetchTopology(endpoint = CALL_ENDPOINT, fetcher = browserF
|
|
|
27
12
|
throw error;
|
|
28
13
|
}
|
|
29
14
|
}
|
|
30
|
-
/**
|
|
31
|
-
* The report read as a graph, local fronds first.
|
|
32
|
-
*
|
|
33
|
-
* Two passes over `edges` rather than one index per direction: a topology is bounded by
|
|
34
|
-
* fronds², so the whole thing is smaller than one page of rows and an index would be a
|
|
35
|
-
* second structure to keep true.
|
|
36
|
-
*/
|
|
15
|
+
/** The report read as a graph, local fronds first. */
|
|
37
16
|
export function nodesOf(report) {
|
|
38
17
|
const order = { local: 0, remote: 1 };
|
|
39
18
|
return [...report.fronds]
|
|
@@ -44,14 +23,7 @@ export function nodesOf(report) {
|
|
|
44
23
|
calledBy: report.edges.filter((edge) => edge.to === frond.frond),
|
|
45
24
|
}));
|
|
46
25
|
}
|
|
47
|
-
/**
|
|
48
|
-
* A frond this process called that publishes nothing of its own shape.
|
|
49
|
-
*
|
|
50
|
-
* The honest reading of `entities: 0, doors: 0` on a remote: its shape is published by the
|
|
51
|
-
* process that owns it, under its own service name — so a panel pointed here can say the
|
|
52
|
-
* frond is reachable and cannot say what it holds. Worth naming rather than drawing as an
|
|
53
|
-
* empty frond, which reads as a frond with nothing in it.
|
|
54
|
-
*/
|
|
26
|
+
/** A frond this process called that publishes nothing of its own shape. */
|
|
55
27
|
export function isOpaque(node) {
|
|
56
28
|
return node.placement === 'remote' && node.entities === 0 && node.doors === 0;
|
|
57
29
|
}
|
package/dist/topology.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topology.js","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"topology.js","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EACL,aAAa,EACb,OAAO,IAAI,cAAc,EACzB,QAAQ,GAET,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,SAAS,GAIV,MAAM,wBAAwB,CAAC;AAIhC,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAQ,GAAG,aAAa,EACxB,OAAO,GAAY,cAAc;IAEjC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CACnB,OAAO,EACP,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,EACjC,gBAAgB,EAChB,QAAQ,CACS,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA4B,EAAE,IAAI,KAAK,SAAS,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QAClF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAUD,sDAAsD;AACtD,MAAM,UAAU,OAAO,CAAC,MAAsB;IAC5C,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACtC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;SACtB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;SACzF,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACf,GAAG,KAAK;QACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,KAAK,CAAC;QAC/D,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,KAAK,CAAC;KACjE,CAAC,CAAC,CAAC;AACR,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,IAAoB;IAC3C,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;AAChF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-alpha.0",
|
|
4
4
|
"description": "A back-office derived from what the app already says it hosts — a react-admin DataProvider over the Fougere wire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"src"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fougere/app": "0.
|
|
38
|
-
"@fougere/core": "0.
|
|
39
|
-
"@fougere/schema": "0.
|
|
37
|
+
"@fougere/app": "0.7.0-alpha.0",
|
|
38
|
+
"@fougere/core": "0.7.0-alpha.0",
|
|
39
|
+
"@fougere/schema": "0.7.0-alpha.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@mui/material": "^9.3.1",
|
package/src/extensions.ts
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Additive presentation changes over the model derived from `rpc.discover`.
|
|
3
|
-
*
|
|
4
|
-
* An extension names only what it changes. It never snapshots a resource, its fields,
|
|
5
|
-
* or its operations, so a field added to the entity tomorrow still appears in the
|
|
6
|
-
* admin. Ordered extensions compose; the last explicit value wins.
|
|
7
|
-
*
|
|
8
|
-
* **It cannot remove a resource or an operation, and that absence is the design.**
|
|
9
|
-
* Both used to carry `hidden`, which hid in the browser what the façade still served
|
|
10
|
-
* — read by anyone as a permission, and enforcing nothing. Membership already has a
|
|
11
|
-
* mechanism: a named surface says what a door serves to one audience, the card
|
|
12
|
-
* answers restricted, and the admin renders what it was told. So the remedy is an
|
|
13
|
-
* endpoint (`/_fougere/call/admin`), not a flag here. A FIELD may still be hidden:
|
|
14
|
-
* that one changes nothing about what is reachable, only about what is drawn.
|
|
15
|
-
*/
|
|
1
|
+
/** Additive presentation changes over the model derived from `rpc.discover`. */
|
|
16
2
|
import type { FormField, TableColumn } from '@fougere/app/client';
|
|
17
3
|
import { capabilitiesOf, type AdminOperation, type AdminResource } from './resources.js';
|
|
18
4
|
import { mergeAdminFacets, type AdminFacets } from './facets.js';
|
package/src/facets.ts
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
import { isObject } from '@fougere/schema';
|
|
2
|
-
/**
|
|
3
|
-
* A semantic notion a renderer can act on, DECLARED — never guessed.
|
|
4
|
-
*
|
|
5
|
-
* A facet says something the schema does not: which field is the human title, which
|
|
6
|
-
* enum member means "published". Both are true statements about a domain, and neither
|
|
7
|
-
* is readable from a shape — `oneOf('draft', 'live')` says the set is closed, never
|
|
8
|
-
* what a member MEANS.
|
|
9
|
-
*
|
|
10
|
-
* There was an `inferAdminFacets` here that answered it by field NAME — `title`,
|
|
11
|
-
* `status`, `/^(user|users|member|account)$/`, `['draft','pending','review']`. It was
|
|
12
|
-
* removed on 2026-08-21: the repo recognises a field by its FORM, never by a word, and
|
|
13
|
-
* that heuristic reads an English vocabulary. An entity spelling `titre`, or a status
|
|
14
|
-
* spelling `brouillon`, lost its facet and nothing said so — a wrong answer given
|
|
15
|
-
* silently, where an absent one is merely absent.
|
|
16
|
-
*
|
|
17
|
-
* So a facet is config, and config is exceptions: no facet is the default, and a
|
|
18
|
-
* renderer that finds none shows the derived table, which is already the whole
|
|
19
|
-
* back-office. What is missing is upstream — the declaration has no way to say what a
|
|
20
|
-
* closed set's member means, and until it has, this is where a human says it.
|
|
21
|
-
*/
|
|
2
|
+
/** A semantic notion a renderer can act on, DECLARED — never guessed. */
|
|
22
3
|
|
|
23
4
|
export interface EditorialFacet {
|
|
24
5
|
/** The field that carries the human-readable title. */
|
|
@@ -46,12 +27,7 @@ export interface UsersFacet {
|
|
|
46
27
|
createdAt?: string;
|
|
47
28
|
}
|
|
48
29
|
|
|
49
|
-
/**
|
|
50
|
-
* Open registry of the semantic projections renderers understand.
|
|
51
|
-
*
|
|
52
|
-
* Fougere ships `editorial` and `users`; a plugin adds `media`, `commerce`,
|
|
53
|
-
* `moderation` without the core renderer growing another enum member.
|
|
54
|
-
*/
|
|
30
|
+
/** Open registry of the semantic projections renderers understand. */
|
|
55
31
|
export interface AdminFacetRegistry {
|
|
56
32
|
editorial: EditorialFacet;
|
|
57
33
|
users: UsersFacet;
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@fougere/admin` — a back-office that states no rule of its own.
|
|
3
|
-
*
|
|
4
|
-
* It reads the identity card and renders what the card says: the doors are the menu,
|
|
5
|
-
* the schema is the columns and the form, the ops are the buttons. Nothing here
|
|
6
|
-
* knows an entity by name, which is why a new one needs no code — and why the same
|
|
7
|
-
* build serves a frond running in this process and one behind `remotes:`.
|
|
8
|
-
*
|
|
9
|
-
* Two layers, kept apart because they do not test the same way and because only the
|
|
10
|
-
* first is derivable: this module is the contract, `./react` is the rendering. That
|
|
11
|
-
* is the line `useFormFor` already draws.
|
|
12
|
-
*/
|
|
1
|
+
/** `@fougere/admin` — a back-office that states no rule of its own. */
|
|
13
2
|
export {
|
|
14
3
|
createDataProvider,
|
|
15
4
|
createLazyDataProvider,
|
package/src/provider.ts
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* react-admin's `DataProvider`, answered by the Fougere wire.
|
|
3
|
-
*
|
|
4
|
-
* Nine functions, and every one of them had a counterpart already: `getList` is
|
|
5
|
-
* `ListOptions`, `getManyReference` is the ORM's `findAllByKeys` (written for a
|
|
6
|
-
* presenter that wanted "the items of these lists"), `filter` is `where` — an
|
|
7
|
-
* equality map that already refuses an unknown key, because a filter silently
|
|
8
|
-
* dropped had once returned the whole table.
|
|
9
|
-
*
|
|
10
|
-
* It talks to the FAÇADE and never to an ORM, which is not a precaution here but a
|
|
11
|
-
* consequence: this runs in a browser, and the only thing a browser can reach is the
|
|
12
|
-
* call endpoint. Judges, presenters and middlewares are therefore on the path of
|
|
13
|
-
* every read and every write the back-office makes.
|
|
14
|
-
*/
|
|
1
|
+
/** react-admin's `DataProvider`, answered by the Fougere wire. */
|
|
15
2
|
import {
|
|
16
3
|
sendCall,
|
|
17
4
|
fetcher as browserFetcher,
|
|
@@ -27,12 +14,7 @@ import { validationErrorsOf } from '@fougere/core/contract';
|
|
|
27
14
|
export interface ResourceKey {
|
|
28
15
|
/** The registration key its door answers under — `post`, not `Post`. */
|
|
29
16
|
name: string;
|
|
30
|
-
/**
|
|
31
|
-
* The field that identifies a row. react-admin insists on `id`; an entity does not,
|
|
32
|
-
* so the provider renames it on the way out and back. `FieldSet.primary` reads it off
|
|
33
|
-
* the card, and answers `undefined` rather than defaulting — a shape with no primary
|
|
34
|
-
* has no row identity to invent.
|
|
35
|
-
*/
|
|
17
|
+
/** The field that identifies an instance. */
|
|
36
18
|
primary: string;
|
|
37
19
|
}
|
|
38
20
|
|
|
@@ -44,16 +26,9 @@ export interface ProviderOptions {
|
|
|
44
26
|
fetcher?: Fetcher;
|
|
45
27
|
}
|
|
46
28
|
|
|
47
|
-
type
|
|
29
|
+
type Values = Record<string, unknown>;
|
|
48
30
|
|
|
49
|
-
/**
|
|
50
|
-
* A refusal, in the shape react-admin reads.
|
|
51
|
-
*
|
|
52
|
-
* `body.errors` keyed by field is what its forms display; `errorsByField` already
|
|
53
|
-
* produces exactly that, and is the same function the Vue and React form primitives
|
|
54
|
-
* use — so a field is highlighted identically whichever door the page came through.
|
|
55
|
-
* A non-validation failure passes through untouched: it is not a form's business.
|
|
56
|
-
*/
|
|
31
|
+
/** A refusal, in the shape react-admin reads. */
|
|
57
32
|
function asAdminError(err: unknown): unknown {
|
|
58
33
|
const refusals = validationErrorsOf(err);
|
|
59
34
|
if (!refusals) return err;
|
|
@@ -72,22 +47,22 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
72
47
|
return known;
|
|
73
48
|
};
|
|
74
49
|
|
|
75
|
-
const call = async (resource: string, op: string,
|
|
50
|
+
const call = async (resource: string, op: string, context: Record<string, unknown>): Promise<unknown> => {
|
|
76
51
|
try {
|
|
77
52
|
return await sendCall(fetcher, { entity: resource, op }, {
|
|
78
|
-
params: {}, query: {},
|
|
53
|
+
params: {}, query: {}, input: undefined, state: {}, ...context,
|
|
79
54
|
}, endpoint);
|
|
80
55
|
} catch (err) {
|
|
81
56
|
throw asAdminError(err);
|
|
82
57
|
}
|
|
83
58
|
};
|
|
84
59
|
|
|
85
|
-
/** The
|
|
86
|
-
const identified = (
|
|
87
|
-
|
|
60
|
+
/** The values as react-admin needs them: whatever identifies them, under `id`. */
|
|
61
|
+
const identified = (values: Values | undefined, key: ResourceKey): Values | undefined =>
|
|
62
|
+
values === undefined || key.primary === 'id' ? values : { ...values, id: values[key.primary] };
|
|
88
63
|
|
|
89
64
|
/** Its dual — what leaves for the server carries the entity's own field name. */
|
|
90
|
-
const deidentified = (data:
|
|
65
|
+
const deidentified = (data: Values, key: ResourceKey): Values => {
|
|
91
66
|
if (key.primary === 'id') return data;
|
|
92
67
|
const { id, ...rest } = data;
|
|
93
68
|
return id === undefined ? rest : { ...rest, [key.primary]: id };
|
|
@@ -96,26 +71,17 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
96
71
|
const list = async (
|
|
97
72
|
resource: string,
|
|
98
73
|
query: Record<string, unknown>,
|
|
99
|
-
): Promise<{ data:
|
|
74
|
+
): Promise<{ data: Values[]; total?: number }> => {
|
|
100
75
|
const key = keyOf(resource);
|
|
101
76
|
const answer = await call(resource, 'list', { query });
|
|
102
77
|
return {
|
|
103
|
-
data: itemsOf<
|
|
78
|
+
data: itemsOf<Values>(answer).map((values) => identified(values, key)!),
|
|
104
79
|
total: pageOf(answer).total,
|
|
105
80
|
};
|
|
106
81
|
};
|
|
107
82
|
|
|
108
83
|
return {
|
|
109
|
-
/**
|
|
110
|
-
* One page, and how it knows there is another.
|
|
111
|
-
*
|
|
112
|
-
* `count: true` is asked for and read when it arrives — but `ListResult` extends
|
|
113
|
-
* `Array`, so `JSON.stringify` drops its `total` and nothing wraps a `page`
|
|
114
|
-
* result today. The same handler therefore answers a total in-process and none
|
|
115
|
-
* over the wire. Rather than depend on it, the page is fetched one row longer
|
|
116
|
-
* than asked: the surplus row IS the answer to "is there a next page", which is
|
|
117
|
-
* `pageInfo`, a form react-admin accepts in place of a total.
|
|
118
|
-
*/
|
|
84
|
+
/** One page, and how it knows there is another. */
|
|
119
85
|
getList: async (resource: string, params: {
|
|
120
86
|
pagination?: { page: number; perPage: number };
|
|
121
87
|
sort?: { field: string; order: string };
|
|
@@ -141,25 +107,21 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
141
107
|
|
|
142
108
|
getOne: async (resource: string, params: { id: string | number }) => {
|
|
143
109
|
const key = keyOf(resource);
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
146
|
-
return { data:
|
|
110
|
+
const values = identified((await call(resource, 'findById', { params: { id: params.id } })) as Values, key);
|
|
111
|
+
if (!values) throw Object.assign(new Error(`${resource} ${params.id} not found`), { status: 404 });
|
|
112
|
+
return { data: values };
|
|
147
113
|
},
|
|
148
114
|
|
|
149
|
-
/**
|
|
150
|
-
* N calls, and the reason is upstream: `findByKeys` is a gesture of the ORM port,
|
|
151
|
-
* not one of `Crud`'s five ops, so there is no door to ask for several rows by id.
|
|
152
|
-
* Said here rather than hidden — the day `Crud` names that op this becomes one call.
|
|
153
|
-
*/
|
|
115
|
+
/** N calls, and the reason is upstream. */
|
|
154
116
|
getMany: async (resource: string, params: { ids: (string | number)[] }) => {
|
|
155
117
|
const key = keyOf(resource);
|
|
156
|
-
const
|
|
118
|
+
const found = await Promise.all(
|
|
157
119
|
params.ids.map((id) => call(resource, 'findById', { params: { id } })),
|
|
158
120
|
);
|
|
159
|
-
return { data:
|
|
121
|
+
return { data: found.map((values) => identified(values as Values, key)).filter((values): values is Values => !!values) };
|
|
160
122
|
},
|
|
161
123
|
|
|
162
|
-
/**
|
|
124
|
+
/** What points AT one — an equality filter, which `where` already is. */
|
|
163
125
|
getManyReference: async (resource: string, params: {
|
|
164
126
|
target: string;
|
|
165
127
|
id: string | number;
|
|
@@ -179,37 +141,31 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
179
141
|
});
|
|
180
142
|
},
|
|
181
143
|
|
|
182
|
-
create: async (resource: string, params: { data:
|
|
144
|
+
create: async (resource: string, params: { data: Values }) => {
|
|
183
145
|
const key = keyOf(resource);
|
|
184
|
-
const
|
|
185
|
-
return { data: identified(
|
|
146
|
+
const values = await call(resource, 'create', { input: deidentified(params.data, key) });
|
|
147
|
+
return { data: identified(values as Values, key)! };
|
|
186
148
|
},
|
|
187
149
|
|
|
188
|
-
update: async (resource: string, params: { id: string | number; data:
|
|
150
|
+
update: async (resource: string, params: { id: string | number; data: Values }) => {
|
|
189
151
|
const key = keyOf(resource);
|
|
190
|
-
const
|
|
152
|
+
const values = await call(resource, 'update', {
|
|
191
153
|
params: { id: params.id },
|
|
192
|
-
|
|
154
|
+
input: deidentified(params.data, key),
|
|
193
155
|
});
|
|
194
|
-
return { data: identified(
|
|
156
|
+
return { data: identified(values as Values, key)! };
|
|
195
157
|
},
|
|
196
158
|
|
|
197
|
-
delete: async (resource: string, params: { id: string | number; previousData?:
|
|
159
|
+
delete: async (resource: string, params: { id: string | number; previousData?: Values }) => {
|
|
198
160
|
await call(resource, 'delete', { params: { id: params.id } });
|
|
199
|
-
return { data: (params.previousData ?? { id: params.id }) as
|
|
161
|
+
return { data: (params.previousData ?? { id: params.id }) as Values };
|
|
200
162
|
},
|
|
201
163
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
* `Together<[…]>` would make them one unit of work — but it is a port a HANDLER
|
|
206
|
-
* asks for, and this is a browser. Making these atomic means naming an operation
|
|
207
|
-
* in the frond that says so, which is the app's sentence to write, not ours.
|
|
208
|
-
*/
|
|
209
|
-
updateMany: async (resource: string, params: { ids: (string | number)[]; data: Row }) => {
|
|
164
|
+
/** The bulk pair, one call per values. */
|
|
165
|
+
updateMany: async (resource: string, params: { ids: (string | number)[]; data: Values }) => {
|
|
210
166
|
const key = keyOf(resource);
|
|
211
167
|
await Promise.all(params.ids.map((id) => call(resource, 'update', {
|
|
212
|
-
params: { id },
|
|
168
|
+
params: { id }, input: deidentified(params.data, key),
|
|
213
169
|
})));
|
|
214
170
|
return { data: params.ids };
|
|
215
171
|
},
|
|
@@ -219,25 +175,14 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
219
175
|
return { data: params.ids };
|
|
220
176
|
},
|
|
221
177
|
|
|
222
|
-
/**
|
|
223
|
-
* A tenth method, because the other nine are CRUD and a Frond is not.
|
|
224
|
-
*
|
|
225
|
-
* react-admin's verbs describe a RESOURCE; `publish` describes an ACTION, and none of
|
|
226
|
-
* the nine can carry it. `DataProvider` has an index signature for exactly this case.
|
|
227
|
-
*
|
|
228
|
-
* **The id rides in `params`, and that is an assumption.** The card states an op's
|
|
229
|
-
* input SCHEMA and never its binding plan — `computeBindingPlan` lives in the boot and
|
|
230
|
-
* does not travel — so a button derived from a card must guess where each argument is
|
|
231
|
-
* read from, and it guesses the CRUD convention. True of every op the scan derives
|
|
232
|
-
* today; the honest fix is upstream, making the binding ride in `CardOp`.
|
|
233
|
-
*/
|
|
178
|
+
/** A tenth method, because the other nine are CRUD and a Frond is not. */
|
|
234
179
|
invoke: async (
|
|
235
180
|
resource: string,
|
|
236
|
-
params: { op: string; id?: string | number; data?:
|
|
181
|
+
params: { op: string; id?: string | number; data?: Values },
|
|
237
182
|
): Promise<{ data: unknown }> => ({
|
|
238
183
|
data: await call(resource, params.op, {
|
|
239
184
|
...(params.id !== undefined ? { params: { id: String(params.id) } } : {}),
|
|
240
|
-
...(params.data !== undefined ? {
|
|
185
|
+
...(params.data !== undefined ? { input: params.data } : {}),
|
|
241
186
|
}),
|
|
242
187
|
}),
|
|
243
188
|
};
|
|
@@ -245,14 +190,7 @@ export function createDataProvider(options: ProviderOptions) {
|
|
|
245
190
|
|
|
246
191
|
export type FougereDataProvider = ReturnType<typeof createDataProvider>;
|
|
247
192
|
|
|
248
|
-
/**
|
|
249
|
-
* The card arrives asynchronously; react-admin requires a provider synchronously.
|
|
250
|
-
*
|
|
251
|
-
* Spell the nine delegates instead of hiding them behind a Proxy. Apart from being
|
|
252
|
-
* inspectable and type-checked, this survives object spreading — the prototype used a
|
|
253
|
-
* Proxy and then spread it into `{}`, which enumerated no methods and handed react-admin
|
|
254
|
-
* an empty provider.
|
|
255
|
-
*/
|
|
193
|
+
/** The card arrives asynchronously; react-admin requires a provider synchronously. */
|
|
256
194
|
export function createLazyDataProvider(load: () => Promise<FougereDataProvider>): FougereDataProvider {
|
|
257
195
|
return {
|
|
258
196
|
getList: async (...args) => (await load()).getList(...args),
|
package/src/react.tsx
CHANGED
|
@@ -103,7 +103,7 @@ function defaultFieldFor(column: TableColumn, t: Translate): ReactElement {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* The bounds ride along under the names a browser already enforces — the same
|
|
106
|
-
* `attrs` a Vue form spreads onto its input. The
|
|
106
|
+
* `attrs` a Vue form spreads onto its input. The validator reads the same shape either
|
|
107
107
|
* way; stating them here only means the refusal arrives while typing.
|
|
108
108
|
*/
|
|
109
109
|
function defaultInputFor(field: FormField, t: Translate): ReactElement {
|
|
@@ -335,7 +335,7 @@ const showFor = (r: AdminResource, renderers?: ReactAdminRenderers) => function
|
|
|
335
335
|
* `useEditController` says it outright (`if (!hasValidationErrors || mutationMode !==
|
|
336
336
|
* 'pessimistic')`), so the per-field refusals `asAdminError` builds from
|
|
337
337
|
* `VALIDATION_FAILED` were computed, sent, and thrown away on every edit. Create was
|
|
338
|
-
* always pessimistic, which is why the same form
|
|
338
|
+
* always pessimistic, which is why the same form validated differently depending on whether
|
|
339
339
|
* the row existed.
|
|
340
340
|
*/
|
|
341
341
|
const editFor = (r: AdminResource, renderers?: ReactAdminRenderers) => function ResourceEdit() {
|
package/src/resources.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the app hosts, read as a back-office.
|
|
3
|
-
*
|
|
4
|
-
* The card (`rpc.discover`) already answers every question a back-office asks: which
|
|
5
|
-
* doors exist, what each one stores, and which of the five verbs it serves. So there
|
|
6
|
-
* is nothing to generate and nothing to configure — a new entity appears in the menu
|
|
7
|
-
* because the card grew a door, not because a file was written.
|
|
8
|
-
*
|
|
9
|
-
* Two projections of the same shape, and they are the pair `form.ts` already draws:
|
|
10
|
-
* `tableColumnsOf` for what a list SHOWS, `formFieldsOf` for what a form SUPPLIES.
|
|
11
|
-
*/
|
|
1
|
+
/** What the app hosts, read as a back-office. */
|
|
12
2
|
import { Card, FieldSet, type SchemaView } from '@fougere/schema';
|
|
13
3
|
import {
|
|
14
4
|
CALL_ENDPOINT,
|
|
@@ -84,13 +74,7 @@ export function capabilitiesOf(operations: readonly Pick<AdminOperation, 'name'>
|
|
|
84
74
|
};
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
/**
|
|
88
|
-
* A door with no schema is not a resource.
|
|
89
|
-
*
|
|
90
|
-
* The card omits `schema` when nothing is stored under that name, and that is
|
|
91
|
-
* ordinary — a health check, a search across several shapes. There is no table to
|
|
92
|
-
* draw for it, and no form. It stays reachable by hand; it is simply not furniture.
|
|
93
|
-
*/
|
|
77
|
+
/** A door with no schema is not a resource. */
|
|
94
78
|
export function resourcesOf(card: IdentityCard): AdminResource[] {
|
|
95
79
|
const out: AdminResource[] = [];
|
|
96
80
|
for (const frond of card.fronds) {
|
|
@@ -126,14 +110,7 @@ export function keysOf(resources: AdminResource[]): Record<string, ResourceKey>
|
|
|
126
110
|
return Object.fromEntries(resources.map((r) => [r.name, { name: r.name, primary: r.primary }]));
|
|
127
111
|
}
|
|
128
112
|
|
|
129
|
-
/**
|
|
130
|
-
* Ask the running app what it hosts.
|
|
131
|
-
*
|
|
132
|
-
* `rpc.discover` travels on the same wire as every other call — it is a reserved op,
|
|
133
|
-
* not a second endpoint — so a back-office needs no configuration to find its
|
|
134
|
-
* subject, and gets the same answer whether the frond is in this process or behind
|
|
135
|
-
* an address in `remotes:`.
|
|
136
|
-
*/
|
|
113
|
+
/** Ask the running app what it hosts. */
|
|
137
114
|
export async function fetchCard(
|
|
138
115
|
endpoint = CALL_ENDPOINT,
|
|
139
116
|
fetcher: Fetcher = browserFetcher,
|
package/src/runtime.ts
CHANGED
|
@@ -38,16 +38,7 @@ export function createAdminRuntime(options: AdminRuntimeOptions = {}): AdminRunt
|
|
|
38
38
|
} = options;
|
|
39
39
|
let loading: Promise<LoadedAdmin> | undefined;
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* One card request, shared — and a REFUSED one is forgotten.
|
|
43
|
-
*
|
|
44
|
-
* `loading ??=` alone memoizes the rejection too, so a receiver that was down when the
|
|
45
|
-
* panel opened stays down for the life of the tab: every later attempt awaits the same
|
|
46
|
-
* dead promise and there is no path back. Clearing the slot on failure is what makes a
|
|
47
|
-
* retry mean anything, and the rejection is re-thrown so the caller can say so — a
|
|
48
|
-
* discovery that fails silently leaves react-admin on its loading page forever, which
|
|
49
|
-
* is the one state where the panel tells the operator nothing at all.
|
|
50
|
-
*/
|
|
41
|
+
/** One card request, shared — and a REFUSED one is forgotten. */
|
|
51
42
|
const load = (): Promise<LoadedAdmin> => loading ??= fetchCard(endpoint, fetcher)
|
|
52
43
|
.then((card) => {
|
|
53
44
|
const resources = applyAdminExtensions(resourcesOf(card), extensions);
|