@fougere/core 0.11.0-alpha.0 → 0.12.1-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/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +18 -0
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +10 -6
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +19 -4
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts +0 -6
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts +2 -9
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +0 -1
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/FrondsStated.d.ts +54 -0
- package/dist/FrondsStated.d.ts.map +1 -0
- package/dist/FrondsStated.js +26 -0
- package/dist/FrondsStated.js.map +1 -0
- package/dist/StatedFrond.d.ts +21 -0
- package/dist/StatedFrond.d.ts.map +1 -0
- package/dist/StatedFrond.js +34 -0
- package/dist/StatedFrond.js.map +1 -0
- package/dist/StatedModules.d.ts +19 -0
- package/dist/StatedModules.d.ts.map +1 -0
- package/dist/StatedModules.js +37 -0
- package/dist/StatedModules.js.map +1 -0
- package/dist/boot/CreateAppOptions.d.ts +19 -2
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts +9 -0
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +11 -5
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +342 -307
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/declared.d.ts.map +1 -1
- package/dist/boot/declared.js +3 -0
- package/dist/boot/declared.js.map +1 -1
- package/dist/boot/frame.d.ts +0 -4
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +31 -14
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +4 -3
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/install.d.ts +9 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +275 -185
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/nesting.d.ts +33 -0
- package/dist/boot/nesting.d.ts.map +1 -0
- package/dist/boot/nesting.js +164 -0
- package/dist/boot/nesting.js.map +1 -0
- package/dist/boot/ownership.d.ts +0 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +66 -51
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/ports.js +30 -25
- package/dist/boot/ports.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +39 -30
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +27 -20
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +71 -56
- package/dist/boot/together.js.map +1 -1
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/descriptor/FrondDescriptor.d.ts +7 -0
- package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
- package/dist/descriptor/MiddlewareEntry.d.ts +6 -5
- package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
- package/dist/descriptor/ProviderEntry.d.ts +7 -0
- package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +23 -20
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/OutputView.d.ts.map +1 -1
- package/dist/dispatch/OutputView.js +7 -0
- package/dist/dispatch/OutputView.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts +6 -0
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +37 -22
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Received.d.ts +3 -0
- package/dist/dispatch/Received.d.ts.map +1 -0
- package/dist/dispatch/Received.js +2 -0
- package/dist/dispatch/Received.js.map +1 -0
- package/dist/dispatch/Release.js +53 -47
- package/dist/dispatch/Release.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts +21 -0
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +84 -66
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/dispatch/decoded.d.ts +19 -0
- package/dist/dispatch/decoded.d.ts.map +1 -0
- package/dist/dispatch/decoded.js +36 -0
- package/dist/dispatch/decoded.js.map +1 -0
- package/dist/entry/facade.d.ts +9 -2
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +16 -7
- package/dist/entry/facade.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +3 -2
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/CrudOps.d.ts +2 -2
- package/dist/prefab/CrudOps.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/Refreshed.d.ts +7 -2
- package/dist/prefab/Refreshed.d.ts.map +1 -1
- package/dist/storage/Storage.js +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +35 -31
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +14 -0
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts +10 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/AppNext.d.ts +2 -1
- package/dist/wire/AppNext.d.ts.map +1 -1
- package/dist/wire/Facade.d.ts +14 -2
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.js +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/Page.d.ts +32 -0
- package/dist/wire/Page.d.ts.map +1 -0
- package/dist/wire/Page.js +32 -0
- package/dist/wire/Page.js.map +1 -0
- package/dist/wire/Signature.d.ts +7 -0
- package/dist/wire/Signature.d.ts.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +29 -57
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js +22 -19
- package/dist/wire/drift.js.map +1 -1
- package/package.json +4 -4
- package/src/EffectiveOperationModel.ts +19 -0
- package/src/FougereConfig.ts +24 -10
- package/src/FrondConfig.ts +0 -6
- package/src/FrondDeclaration.ts +2 -7
- package/src/FrondsStated.ts +72 -0
- package/src/StatedFrond.ts +51 -0
- package/src/StatedModules.ts +58 -0
- package/src/boot/CreateAppOptions.ts +19 -2
- package/src/boot/Emissions.ts +16 -5
- package/src/boot/bootstrap.ts +478 -359
- package/src/boot/declared.ts +3 -0
- package/src/boot/frame.ts +42 -11
- package/src/boot/hosted.ts +4 -3
- package/src/boot/install.ts +392 -212
- package/src/boot/nesting.ts +196 -0
- package/src/boot/ownership.ts +79 -51
- package/src/boot/ports.ts +45 -31
- package/src/boot/remote.ts +57 -32
- package/src/boot/seed.ts +36 -27
- package/src/boot/together.ts +98 -63
- package/src/contract.ts +2 -0
- package/src/descriptor/FrondDescriptor.ts +7 -0
- package/src/descriptor/MiddlewareEntry.ts +6 -5
- package/src/descriptor/ProviderEntry.ts +7 -0
- package/src/dispatch/HandlerFacade.ts +27 -17
- package/src/dispatch/OutputView.ts +7 -0
- package/src/dispatch/PresenterExecutor.ts +40 -22
- package/src/dispatch/Received.ts +2 -0
- package/src/dispatch/Release.ts +68 -44
- package/src/dispatch/StorageGuard.ts +90 -69
- package/src/dispatch/decoded.ts +37 -0
- package/src/entry/facade.ts +21 -9
- package/src/index.ts +4 -0
- package/src/node.ts +2 -1
- package/src/prefab/CrudConstructor.ts +3 -2
- package/src/prefab/CrudOps.ts +2 -2
- package/src/prefab/MirrorConstructor.ts +1 -1
- package/src/prefab/Refreshed.ts +7 -2
- package/src/storage/Storage.ts +2 -2
- package/src/storage/Store.ts +45 -27
- package/src/verify.ts +15 -0
- package/src/wire/AppMiddleware.ts +10 -1
- package/src/wire/AppNext.ts +2 -1
- package/src/wire/Facade.ts +14 -2
- package/src/wire/FougereError.ts +1 -1
- package/src/wire/Invocation.ts +2 -1
- package/src/wire/OperationContract.ts +1 -1
- package/src/wire/Page.ts +50 -0
- package/src/wire/Signature.ts +7 -0
- package/src/wire/binding.ts +26 -58
- package/src/wire/drift.ts +48 -21
package/src/boot/install.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Putting one frond into the app being built: its scope, what it serves, what it takes. */
|
|
2
2
|
import type { Container } from '@fougere/container';
|
|
3
|
+
import type { ProviderEntry } from '../descriptor/ProviderEntry.js';
|
|
4
|
+
import type { PresenterEntry } from '../descriptor/PresenterEntry.js';
|
|
3
5
|
import { lowerFirst, type Fields, type SchemaView } from '@fougere/schema';
|
|
4
6
|
import type { Logger } from '../builtin/Logger.js';
|
|
5
7
|
import type { Dispatcher } from '../dispatch/Dispatcher.js';
|
|
@@ -35,6 +37,7 @@ import { RouteAddress } from '../wire/RouteAddress.js';
|
|
|
35
37
|
import { servedSurfaces } from '../descriptor/surface.js';
|
|
36
38
|
import { OperationRoute } from '../dispatch/OperationRoute.js';
|
|
37
39
|
import { facadeOperations } from '../entry/facade.js';
|
|
40
|
+
import { decoded } from '../dispatch/decoded.js';
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* What the app is made of while it is still being made. `createApp` builds these, hands
|
|
@@ -71,84 +74,220 @@ export interface Assembly {
|
|
|
71
74
|
getMiddlewares: (entity: string) => AppMiddleware[];
|
|
72
75
|
/** Take a middleware on — every entity when no entity is named. */
|
|
73
76
|
use: (middleware: AppMiddleware, entity?: string) => void;
|
|
77
|
+
/**
|
|
78
|
+
* What runs around each frond, its ancestors' included — filled as fronds install, read by
|
|
79
|
+
* their children. A middleware is not a container key, so inheriting one is a list to carry
|
|
80
|
+
* and not a scope to walk.
|
|
81
|
+
*/
|
|
82
|
+
middlewaresOf: Map<string, AppMiddleware[]>;
|
|
83
|
+
/** What wraps each frond's seams, its ancestors' included — read the same way. */
|
|
84
|
+
seamsOf: Map<string, Map<string, ProviderEntry[]>>;
|
|
74
85
|
log: Logger;
|
|
75
86
|
options: CreateAppOptions;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
/**
|
|
90
|
+
* A cross-source reader over the entities this frond NAMED — resolved across the whole app,
|
|
91
|
+
* because such a query joins entities of different fronds by definition. Naming one IS the
|
|
92
|
+
* authorization; that is what the declaration is for.
|
|
93
|
+
*/
|
|
94
|
+
async function registerReads(
|
|
95
|
+
frond: FrondDescriptor,
|
|
96
|
+
scope: Container,
|
|
97
|
+
entityByName: Map<string, unknown>,
|
|
98
|
+
sourcesFactory: CreateAppOptions['sourcesFactory'],
|
|
99
|
+
frondLog: Logger,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!frond.reads?.length) return;
|
|
84
102
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const key = facadeKeyOf(handler.address, handler.surface);
|
|
92
|
-
const operations = operationModel.forHandler(handler);
|
|
93
|
-
effectiveByKey.set(key, operations);
|
|
94
|
-
emissions.note(contractsOf(operations), key);
|
|
103
|
+
if (!sourcesFactory) {
|
|
104
|
+
frondLog.warn(
|
|
105
|
+
`[reads] ${frond.reads.join(', ')} — declared in frond.config.ts, but this boot passes no `
|
|
106
|
+
+ '`sourcesFactory`, so no reader is registered and a handler asking for `Reads` will fail '
|
|
107
|
+
+ 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
|
|
108
|
+
);
|
|
95
109
|
|
|
96
|
-
}
|
|
97
110
|
return;
|
|
98
111
|
}
|
|
99
|
-
const scope = container.createScope();
|
|
100
|
-
const frondLog = log.child(frond.name);
|
|
101
|
-
// The frond's own voice, a child of the APP logger and never of `log` — which is the
|
|
102
|
-
// boot's, so a service's line would have claimed `boot:` long after the boot was over.
|
|
103
|
-
scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// clause: the handler asking for `Reads` then dies at its first call, on a
|
|
111
|
-
// container message that names neither the clause nor what is missing.
|
|
112
|
-
if (frond.reads?.length && !options.sourcesFactory) {
|
|
113
|
+
const named = frond.reads
|
|
114
|
+
.map((name) => entityByName.get(lowerFirst(name)))
|
|
115
|
+
.filter((entity): entity is NonNullable<typeof entity> => entity !== undefined);
|
|
116
|
+
if (named.length !== frond.reads.length) {
|
|
117
|
+
const missing = frond.reads.filter((name) => !entityByName.has(lowerFirst(name)));
|
|
113
118
|
frondLog.warn(
|
|
114
|
-
`[reads] ${
|
|
115
|
-
+ '
|
|
116
|
-
+ 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
|
|
119
|
+
`[reads] ${missing.join(', ')} — named in frond.config.ts but scanned nowhere in this app, `
|
|
120
|
+
+ 'so a query naming one would find no table. Check the spelling, or the entity file.',
|
|
117
121
|
);
|
|
118
122
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
|
|
124
|
+
scope.registerValue('Reads', await sourcesFactory(named, frond.name));
|
|
125
|
+
frondLog.debug(`cross-source reader over ${named.length} entit(ies)`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* ONE instance per frond scope, built at its first call and closed with its frond.
|
|
130
|
+
*
|
|
131
|
+
* Its only consumer is the dispatch, which lives as long as the app, so its lifetime is not a
|
|
132
|
+
* choice. Resolved at the call and never at boot: a dependency may be registered by an
|
|
133
|
+
* extension's `up`, which rises after every frond is installed.
|
|
134
|
+
*/
|
|
135
|
+
function registerMiddlewares(
|
|
136
|
+
frond: FrondDescriptor,
|
|
137
|
+
scope: Container,
|
|
138
|
+
assembly: Assembly,
|
|
139
|
+
frondLog: Logger,
|
|
140
|
+
): void {
|
|
141
|
+
const { use, middlewaresOf } = assembly;
|
|
142
|
+
// Its own frond means every address its handlers answer to — wider than its entities, since
|
|
143
|
+
// a handler without one runs behind it too. A frond that serves nothing has none, which is
|
|
144
|
+
// why what it declares only ever reaches its family.
|
|
145
|
+
const addresses = new Set(frond.handlers.map((h) => h.address));
|
|
146
|
+
// What the parent took on, its own ancestors included — one level up is the whole chain,
|
|
147
|
+
// because the boot installs parents first and each of them left theirs here. The closures
|
|
148
|
+
// resolve in the scope that built them, so an inherited middleware is handed the services of
|
|
149
|
+
// the frond that declared it.
|
|
150
|
+
const inherited = frond.extends ? middlewaresOf.get(frond.extends) ?? [] : [];
|
|
151
|
+
const mine: AppMiddleware[] = [];
|
|
152
|
+
|
|
153
|
+
for (const middleware of frond.middlewares) {
|
|
154
|
+
scope.register(middleware.name, middleware.ctor, { deps: middleware.deps, lifetime: 'singleton' });
|
|
155
|
+
mine.push((context, next) =>
|
|
156
|
+
scope.resolve<{ around: AppMiddleware }>(middleware.name).around(context, next));
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
// Ancestors first, so they stand OUTSIDE — `getMiddlewares` hands back insertion order.
|
|
160
|
+
for (const around of [...inherited, ...mine]) {
|
|
161
|
+
for (const address of addresses) use(around, address);
|
|
162
|
+
}
|
|
163
|
+
middlewaresOf.set(frond.name, [...inherited, ...mine]);
|
|
164
|
+
|
|
165
|
+
if (frond.middlewares.length > 0) {
|
|
166
|
+
frondLog.debug(`${frond.middlewares.length} middleware(s): ${frond.middlewares.map((m) => m.name).join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What wraps a seam here: what the parent already put in front, then this frond's own. */
|
|
171
|
+
function inheritedSeams(
|
|
172
|
+
frond: FrondDescriptor,
|
|
173
|
+
seamsOf: Map<string, Map<string, ProviderEntry[]>>,
|
|
174
|
+
declared: Map<string, ProviderEntry[]>,
|
|
175
|
+
): Map<string, ProviderEntry[]> {
|
|
176
|
+
const above = frond.extends ? seamsOf.get(frond.extends) : undefined;
|
|
177
|
+
if (!above) return declared;
|
|
178
|
+
|
|
179
|
+
const all = new Map(declared);
|
|
180
|
+
for (const [seam, links] of above) all.set(seam, [...links, ...(declared.get(seam) ?? [])]);
|
|
181
|
+
|
|
182
|
+
return all;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* One storage per entity, and the default repository that IS it.
|
|
187
|
+
*
|
|
188
|
+
* The declared chain first, then the guard OUTSIDE it: the guard hands on the value it parsed,
|
|
189
|
+
* so a link reads what the entity says a row is rather than what arrived.
|
|
190
|
+
*/
|
|
191
|
+
function registerStorages(
|
|
192
|
+
frond: FrondDescriptor,
|
|
193
|
+
assembly: Assembly,
|
|
194
|
+
scope: Container,
|
|
195
|
+
seams: Map<string, ProviderEntry[]>,
|
|
196
|
+
owners: Map<string, string>,
|
|
197
|
+
frondLog: Logger,
|
|
198
|
+
): void {
|
|
199
|
+
const { options, refused } = assembly;
|
|
200
|
+
if (!options.storageFactory) return;
|
|
201
|
+
|
|
202
|
+
const unenforced: string[] = [];
|
|
203
|
+
const release = releasing(assembly.hosting);
|
|
204
|
+
for (const entity of frond.entities) {
|
|
205
|
+
const key = storageKeyOf(entity.name);
|
|
206
|
+
const source = options.sourceOf?.(entity.name) ?? 'db';
|
|
207
|
+
if (declares(entity.entityClass, 'unique') && options.enforces?.(source, 'unique') === false) {
|
|
208
|
+
unenforced.push(`${entity.name} in '${source}'`);
|
|
209
|
+
}
|
|
210
|
+
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
211
|
+
|
|
212
|
+
// Check if the default handler (no surface) declares an output override
|
|
213
|
+
const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
|
|
214
|
+
const outputSchema = defaultHandler?.outputOverride ?? (defaultHandler?.ctor as { __output?: SchemaView } | undefined)?.__output;
|
|
215
|
+
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
216
|
+
? baseStorage.output(outputSchema)
|
|
217
|
+
: baseStorage;
|
|
218
|
+
|
|
219
|
+
// The declared chain first, then the guard OUTSIDE it: the guard hands on the value
|
|
220
|
+
// it parsed, so a wrapper reads what the entity says a row is rather than what
|
|
221
|
+
// arrived. Same order the client facade has held since `StorageGuard` existed.
|
|
222
|
+
const linked = wrapping('Storage', seams.get('Storage') ?? [], scoped, (dep) => scope.resolve(dep));
|
|
223
|
+
// Storage is a way out like the client surface — see `StorageGuard`.
|
|
224
|
+
refused.push(...refuseUnwritableNull(entity.entityClass, entity.name, entity.filePath));
|
|
225
|
+
const relations = heldBy(entity.entityClass, entity.name, assembly.hosting);
|
|
226
|
+
assembly.relations.push(...relations);
|
|
227
|
+
const guarded = new StorageGuard(
|
|
228
|
+
entity.entityClass.getFields(), entity.name, {}, relations, release,
|
|
229
|
+
).guard(linked);
|
|
230
|
+
scope.registerValue(key, guarded);
|
|
144
231
|
|
|
232
|
+
// The default repository IS the guarded port — it already answers every gesture a
|
|
233
|
+
// declared one forwards, so the two forms have the same shape and a handler reads
|
|
234
|
+
// `repo.list()` either way. The wrapper that used to sit here (`{ storage: guarded }`)
|
|
235
|
+
// existed to make `repo.storage` true in both, back when `.storage` was the way in.
|
|
236
|
+
//
|
|
237
|
+
// Not registered for an OWNED entity: an aggregate's members are reached through it
|
|
238
|
+
// and nowhere else, and the default would be a second facade under a name a handler
|
|
239
|
+
// can spell. Every member is skipped, not just the one the key is named after —
|
|
240
|
+
// that asymmetry was the whole hole.
|
|
241
|
+
const repoKey = repositoryKeyOf(entity.name);
|
|
242
|
+
const owner = owners.get(entity.name);
|
|
243
|
+
if (owner) {
|
|
244
|
+
frondLog.debug(`${entity.name} — owned by ${owner}, no default repository`);
|
|
245
|
+
} else if (!scope.has(repoKey)) {
|
|
246
|
+
scope.registerValue(repoKey, guarded);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (frond.entities.length > 0) {
|
|
250
|
+
frondLog.debug(`${frond.entities.length} entity storage(s): ${frond.entities.map((e) => e.name).join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
// The judge refuses a duplicate it can SEE — the row already stored. Two writes arriving
|
|
253
|
+
// together see the same absence, and only the place they land can refuse the second.
|
|
254
|
+
if (unenforced.length > 0) {
|
|
255
|
+
frondLog.warn(
|
|
256
|
+
`unique declared, and the source does not enforce it: ${unenforced.join(', ')} — `
|
|
257
|
+
+ 'two concurrent writes can both pass',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Every provider under its own name, then under the PORT each one extends — so
|
|
264
|
+
* `private payment: Payment` reaches the realization instead of the base it is declared
|
|
265
|
+
* against. The port key is registered AFTER, so it always wins over the base's own.
|
|
266
|
+
*
|
|
267
|
+
* Gives back the seam chains: a seam is bound where its realization is BUILT, not under a
|
|
268
|
+
* container key, so `registerStorages` is the one that puts them in front of something.
|
|
269
|
+
*/
|
|
270
|
+
function registerProviders(
|
|
271
|
+
frond: FrondDescriptor,
|
|
272
|
+
scope: Container,
|
|
273
|
+
ports: CreateAppOptions['ports'],
|
|
274
|
+
boundPorts: Set<string>,
|
|
275
|
+
refused: Diagnostic[],
|
|
276
|
+
frondLog: Logger,
|
|
277
|
+
): Map<string, ProviderEntry[]> {
|
|
145
278
|
for (const provider of frond.providers) {
|
|
146
|
-
scope
|
|
279
|
+
// `kept` is `implements AsyncDisposable` — one per frond scope, closed when it closes.
|
|
280
|
+
// Without it a provider is built per consumer and closed by nobody, which is what a
|
|
281
|
+
// service holding nothing wants.
|
|
282
|
+
scope.register(nameOf(provider), provider.ctor, {
|
|
283
|
+
deps: provider.deps,
|
|
284
|
+
...(provider.kept ? { lifetime: 'singleton' as const } : {}),
|
|
285
|
+
});
|
|
147
286
|
}
|
|
148
287
|
// What this frond puts in front of one of the framework's own ports. Its own, like every
|
|
149
288
|
// provider — a link goes where its frond goes, which is what a frond behind `remotes:`
|
|
150
289
|
// takes with it.
|
|
151
|
-
const seams = seamChains(frond.providers,
|
|
290
|
+
const seams = seamChains(frond.providers, ports, refused);
|
|
152
291
|
for (const [seam, links] of seams) {
|
|
153
292
|
boundPorts.add(seam);
|
|
154
293
|
frondLog.debug(`seam ${seam} → ${links.map((one) => one.ctor.name).join(' → ')} → the realization`);
|
|
@@ -157,7 +296,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
157
296
|
// reaches the realization instead of the base class it is declared against.
|
|
158
297
|
// Registered AFTER the loop above so a port key always wins over the base's
|
|
159
298
|
// own registration — same precedence as a declared repository over its default.
|
|
160
|
-
for (const [port, chain] of portBindings(frond.providers, (n) => scope.has(n),
|
|
299
|
+
for (const [port, chain] of portBindings(frond.providers, (n) => scope.has(n), ports, refused)) {
|
|
161
300
|
// A chain a refusal emptied — `ports:` named a class that extends nothing. Already
|
|
162
301
|
// reported, and there is no realization left to put a key in front of.
|
|
163
302
|
if (chain.length === 0) continue;
|
|
@@ -184,68 +323,196 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
184
323
|
frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map(nameOf).join(', ')}`);
|
|
185
324
|
}
|
|
186
325
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
326
|
+
return seams;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* A named surface's own storage — the same rows, scoped to what this facade shows.
|
|
331
|
+
*
|
|
332
|
+
* Under the REPOSITORY key, which is what a Crud handler asks for, and under the port's own for
|
|
333
|
+
* a holder that legitimately names it: registering only the latter left a named surface with no
|
|
334
|
+
* facade at all once the façade stopped spelling the storage.
|
|
335
|
+
*/
|
|
336
|
+
function registerSurfaceStorage(
|
|
337
|
+
entity: EntityEntry,
|
|
338
|
+
handler: HandlerEntry,
|
|
339
|
+
surfaceScope: Container,
|
|
340
|
+
assembly: Assembly,
|
|
341
|
+
frondLog: Logger,
|
|
342
|
+
): void {
|
|
343
|
+
const { options } = assembly;
|
|
344
|
+
if (!options.storageFactory) return;
|
|
345
|
+
|
|
346
|
+
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
347
|
+
const outputSchema = handler.outputOverride ?? (handler.ctor as { __output?: SchemaView }).__output;
|
|
348
|
+
const narrowed = outputSchema && outputSchema !== entity.entityClass;
|
|
349
|
+
const scoped = narrowed ? baseStorage.output(outputSchema) : baseStorage;
|
|
350
|
+
// The view is handed over so a filter on a field this facade hides is SAID. The guard holds
|
|
351
|
+
// no logger — a warning is the boot's to voice, as a seed's report is.
|
|
352
|
+
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name, {
|
|
353
|
+
...(narrowed ? { view: (outputSchema as { getFields(): Fields }).getFields() } : {}),
|
|
354
|
+
outOfView: (message) => frondLog.warn(message),
|
|
355
|
+
}, heldBy(entity.entityClass, entity.name, assembly.hosting), releasing(assembly.hosting)).guard(scoped);
|
|
356
|
+
|
|
357
|
+
surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
|
|
358
|
+
surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A presenter is about an entity — computed fields sit on a shape — so this walks entities.
|
|
363
|
+
* Exposed lazily: the bridge resolves the instance on first access.
|
|
364
|
+
*/
|
|
365
|
+
function exposePresenters(
|
|
366
|
+
frond: FrondDescriptor,
|
|
367
|
+
presenterMap: Map<string, unknown>,
|
|
368
|
+
container: Container,
|
|
369
|
+
scope: Container,
|
|
370
|
+
): void {
|
|
371
|
+
for (const entity of frond.entities) {
|
|
372
|
+
if (!presenterMap.has(entity.name)) continue;
|
|
373
|
+
|
|
374
|
+
const presenterKey = presenterKeyOf(entity.name);
|
|
375
|
+
let instance: Record<string | symbol, unknown> | undefined;
|
|
376
|
+
container.registerValue(presenterKey, new Proxy({} as Record<string, unknown>, {
|
|
377
|
+
get(_target, prop) {
|
|
378
|
+
instance ??= scope.resolve<Record<string | symbol, unknown>>(presenterKey);
|
|
379
|
+
|
|
380
|
+
return instance[prop];
|
|
381
|
+
},
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** What a facade is built out of, for one frond. */
|
|
387
|
+
interface Building {
|
|
388
|
+
frond: FrondDescriptor;
|
|
389
|
+
assembly: Assembly;
|
|
390
|
+
/** The frond's own, which a presenter is resolved through whatever audience the facade serves. */
|
|
391
|
+
scope: Container;
|
|
392
|
+
collectorTypeNames: Set<string>;
|
|
393
|
+
presenterMap: Map<string, PresenterEntry>;
|
|
394
|
+
frondLog: Logger;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Build the facade of a handler, and register it under the audience it serves. */
|
|
398
|
+
function buildFacadeInto(
|
|
399
|
+
{ frond, assembly, scope, collectorTypeNames, presenterMap, frondLog }: Building,
|
|
400
|
+
entity: EntityEntry | undefined,
|
|
401
|
+
handler: HandlerEntry,
|
|
402
|
+
targetScope: Container,
|
|
403
|
+
facadeKey: string,
|
|
404
|
+
): void {
|
|
405
|
+
const {
|
|
406
|
+
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
407
|
+
operationModel, getMiddlewares,
|
|
408
|
+
} = assembly;
|
|
409
|
+
|
|
410
|
+
if (inheritsCrud(handler.ctor) && !entity) {
|
|
411
|
+
// An installed Crud subject may be absent from the local scan.
|
|
412
|
+
frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
|
|
413
|
+
+ `'${subjectOf(handler.ctor, handler.address)}' — installed entity, or a missing `
|
|
414
|
+
+ `one: no storage will be injected`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const facade = new HandlerFacade(handler, targetScope, {
|
|
418
|
+
key: facadeKey,
|
|
419
|
+
frond: frond.name,
|
|
420
|
+
handlers: frond.handlers,
|
|
421
|
+
operations: operationModel.forHandler(handler),
|
|
422
|
+
collectors: collectorTypeNames,
|
|
423
|
+
presenter: presenterMap.get(handler.address),
|
|
424
|
+
presenterScope: scope,
|
|
425
|
+
middlewares: () => getMiddlewares(handler.address),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Emissions use the same contracts and execution path as direct calls, and the terms sit
|
|
429
|
+
// under the same audience — a surface that serves fewer ops describes fewer ops.
|
|
430
|
+
emissions.note(facade.contracts, facadeKey);
|
|
431
|
+
container.registerValue(contractsKeyOf(handler.address, handler.surface), facade.contracts);
|
|
432
|
+
effectiveByKey.set(facadeKey, facade.effectiveOperations);
|
|
433
|
+
|
|
434
|
+
for (const operation of facade.contracts.keys()) {
|
|
435
|
+
for (const surface of servedSurfaces(frond, handler)) {
|
|
436
|
+
routeRegistry.register(new OperationRoute(
|
|
437
|
+
'local',
|
|
438
|
+
new RouteAddress({
|
|
439
|
+
entity: handler.address,
|
|
440
|
+
operation,
|
|
441
|
+
...(surface !== undefined ? { surface } : {}),
|
|
442
|
+
}),
|
|
443
|
+
(call) => facade.execute(operation, call.invocation),
|
|
444
|
+
));
|
|
239
445
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
container.registerValue(facadeKey, facadeOperations(
|
|
449
|
+
handler.surface ? localDispatcher : dispatcher,
|
|
450
|
+
handler.address,
|
|
451
|
+
routeRegistry.operationNames(handler.address, handler.surface),
|
|
452
|
+
handler.surface,
|
|
453
|
+
// The output SCHEMA, per op: `Crud(Post, { list: PostCard })` names a different one for
|
|
454
|
+
// one operation, so the answer is read back through the view that op actually serves.
|
|
455
|
+
(operation, answer) => decoded(facade.effectiveOperations.get(operation)?.output, answer),
|
|
456
|
+
));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
|
|
460
|
+
const {
|
|
461
|
+
container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
|
|
462
|
+
boundPorts, refused, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
|
|
463
|
+
log, options,
|
|
464
|
+
} = assembly;
|
|
465
|
+
|
|
466
|
+
// Declared remote: keep the scanned metadata (bridges route with it),
|
|
467
|
+
// register nothing locally — resolve() falls through to the remote façade.
|
|
468
|
+
if (options.remotes && frond.name in options.remotes) {
|
|
469
|
+
log.child(frond.name).info('declared remote — not hosted locally');
|
|
470
|
+
// Its facades answer elsewhere, but what they LISTEN to was read here.
|
|
471
|
+
for (const handler of frond.handlers) {
|
|
472
|
+
const key = facadeKeyOf(handler.address, handler.surface);
|
|
473
|
+
const operations = operationModel.forHandler(handler);
|
|
474
|
+
effectiveByKey.set(key, operations);
|
|
475
|
+
emissions.note(contractsOf(operations), key);
|
|
476
|
+
|
|
247
477
|
}
|
|
478
|
+
return;
|
|
248
479
|
}
|
|
480
|
+
// A child hangs off its parent, so everything the parent registered answers here too and
|
|
481
|
+
// nothing else has to know: `ScopeContainer.resolve` already walks up. The boot installs
|
|
482
|
+
// parents first, which is what makes the key below resolvable.
|
|
483
|
+
const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
|
|
484
|
+
const scope = above.createScope();
|
|
485
|
+
const frondLog = log.child(frond.name);
|
|
486
|
+
// The frond's own voice, a child of the APP logger and never of `log` — which is the
|
|
487
|
+
// boot's, so a service's line would have claimed `boot:` long after the boot was over.
|
|
488
|
+
scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
|
|
489
|
+
|
|
490
|
+
// `reads:` is what makes a cross-source reader exist here, and the list IS its
|
|
491
|
+
// environment — a source holding none of these is never opened. Registered under
|
|
492
|
+
// the type's own name, which is the key `depKeyOf` already derives for a plain
|
|
493
|
+
// parameter: `constructor(private reads: Reads)` and nothing else to say.
|
|
494
|
+
// Declaring `reads:` with nothing to build the reader is a boot that ignores a
|
|
495
|
+
// clause: the handler asking for `Reads` then dies at its first call, on a
|
|
496
|
+
// container message that names neither the clause nor what is missing.
|
|
497
|
+
await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
|
|
498
|
+
|
|
499
|
+
// Who owns what, and the rule that makes owning mean something. Before anything is
|
|
500
|
+
// registered, so a bad line is named by this refusal rather than by the container's.
|
|
501
|
+
sharedNames(frond, refused);
|
|
502
|
+
const owners = ownersOf(frond.providers, frond.name, refused);
|
|
503
|
+
storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
|
|
504
|
+
crudOnOwned(frond, owners, refused);
|
|
505
|
+
|
|
506
|
+
const declared = registerProviders(frond, scope, options.ports, boundPorts, refused, frondLog);
|
|
507
|
+
// An ancestor's links stand OUTSIDE this frond's own, the order `wrapping` reads — and
|
|
508
|
+
// like a middleware, a link cannot be inherited through a container key, since a seam has
|
|
509
|
+
// none: its realization is built rather than resolved.
|
|
510
|
+
const seams = inheritedSeams(frond, assembly.seamsOf, declared);
|
|
511
|
+
assembly.seamsOf.set(frond.name, seams);
|
|
512
|
+
|
|
513
|
+
// Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
|
|
514
|
+
// When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
|
|
515
|
+
registerStorages(frond, assembly, scope, seams, owners, frondLog);
|
|
249
516
|
|
|
250
517
|
// Frames, after the ORMs and before anything that may ask for one. A frame is read
|
|
251
518
|
// from the same `deps` every other port is read from — asking for it IS declaring it,
|
|
@@ -291,95 +558,24 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
291
558
|
// Register middlewares in scope, then take them on. Resolved per call and never here:
|
|
292
559
|
// a middleware asking for something request-scoped would otherwise be handed the one
|
|
293
560
|
// instance the boot built — the same reason `getMiddlewares` is read at call time.
|
|
294
|
-
|
|
295
|
-
scope.register(middleware.name, middleware.ctor, { deps: middleware.deps });
|
|
296
|
-
const around: AppMiddleware = (context, next) =>
|
|
297
|
-
scope.resolve<{ around: AppMiddleware }>(middleware.name).around(context, next);
|
|
298
|
-
|
|
299
|
-
if (middleware.scope === 'app') use(around);
|
|
300
|
-
// Its own frond means every address its handlers answer to — wider than its entities,
|
|
301
|
-
// since a handler without one runs behind it too.
|
|
302
|
-
else for (const address of new Set(frond.handlers.map((h) => h.address))) use(around, address);
|
|
303
|
-
}
|
|
304
|
-
if (frond.middlewares.length > 0) {
|
|
305
|
-
frondLog.debug(`${frond.middlewares.length} middleware(s): ${frond.middlewares.map((m) => `${m.name} (${m.scope})`).join(', ')}`);
|
|
306
|
-
}
|
|
561
|
+
registerMiddlewares(frond, scope, assembly, frondLog);
|
|
307
562
|
|
|
308
563
|
// Build handler facades → registered in ROOT container (public contract)
|
|
309
564
|
const defaultHandlers = frond.handlers.filter((h) => !h.surface);
|
|
310
565
|
const surfaceHandlers = frond.handlers.filter((h) => h.surface);
|
|
311
566
|
const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
|
|
312
567
|
|
|
313
|
-
|
|
568
|
+
const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
|
|
314
569
|
const buildFacade = (
|
|
315
570
|
entity: EntityEntry | undefined,
|
|
316
571
|
handler: HandlerEntry,
|
|
317
572
|
targetScope: Container,
|
|
318
573
|
facadeKey: string,
|
|
319
|
-
) =>
|
|
320
|
-
if (inheritsCrud(handler.ctor) && !entity) {
|
|
321
|
-
// An installed Crud subject may be absent from the local scan.
|
|
322
|
-
frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
|
|
323
|
-
+ `'${subjectOf(handler.ctor, handler.address)}' — installed entity, or a missing `
|
|
324
|
-
+ `one: no storage will be injected`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const facade = new HandlerFacade(handler, targetScope, {
|
|
328
|
-
key: facadeKey,
|
|
329
|
-
frond: frond.name,
|
|
330
|
-
handlers: frond.handlers,
|
|
331
|
-
operations: operationModel.forHandler(handler),
|
|
332
|
-
collectors: collectorTypeNames,
|
|
333
|
-
presenter: presenterMap.get(handler.address),
|
|
334
|
-
presenterScope: scope,
|
|
335
|
-
middlewares: () => getMiddlewares(handler.address),
|
|
336
|
-
});
|
|
337
|
-
// Emissions use the same contracts and execution path as direct calls.
|
|
338
|
-
emissions.note(facade.contracts, facadeKey);
|
|
339
|
-
// The terms alongside the facade, under the same audience — a surface that serves
|
|
340
|
-
// fewer ops describes fewer ops.
|
|
341
|
-
container.registerValue(contractsKeyOf(handler.address, handler.surface), facade.contracts);
|
|
342
|
-
effectiveByKey.set(facadeKey, facade.effectiveOperations);
|
|
343
|
-
|
|
344
|
-
const surfaces = servedSurfaces(frond, handler);
|
|
345
|
-
|
|
346
|
-
for (const operation of facade.contracts.keys()) {
|
|
347
|
-
for (const surface of surfaces) {
|
|
348
|
-
const address = new RouteAddress({
|
|
349
|
-
entity: handler.address,
|
|
350
|
-
operation,
|
|
351
|
-
...(surface !== undefined ? { surface } : {}),
|
|
352
|
-
});
|
|
353
|
-
routeRegistry.register(new OperationRoute(
|
|
354
|
-
'local',
|
|
355
|
-
address,
|
|
356
|
-
(call) => facade.execute(operation, call.invocation),
|
|
357
|
-
));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const operations = facadeOperations(
|
|
362
|
-
handler.surface ? localDispatcher : dispatcher,
|
|
363
|
-
handler.address,
|
|
364
|
-
routeRegistry.operationNames(handler.address, handler.surface),
|
|
365
|
-
handler.surface,
|
|
366
|
-
);
|
|
367
|
-
container.registerValue(facadeKey, operations);
|
|
368
|
-
};
|
|
574
|
+
) => buildFacadeInto(building, entity, handler, targetScope, facadeKey);
|
|
369
575
|
|
|
370
576
|
// A presenter is about an entity — computed fields sit on a shape — so this walks
|
|
371
577
|
// entities. Exposing the instance lazily; the bridge resolves it on first access.
|
|
372
|
-
|
|
373
|
-
if (!presenterMap.has(entity.name)) continue;
|
|
374
|
-
const presenterKey = presenterKeyOf(entity.name);
|
|
375
|
-
let presenterInstance: any;
|
|
376
|
-
container.registerValue(presenterKey, new Proxy({} as any, {
|
|
377
|
-
get(_target, prop) {
|
|
378
|
-
if (!presenterInstance) presenterInstance = scope.resolve(presenterKey);
|
|
379
|
-
return presenterInstance[prop];
|
|
380
|
-
},
|
|
381
|
-
}));
|
|
382
|
-
}
|
|
578
|
+
exposePresenters(frond, presenterMap, container, scope);
|
|
383
579
|
|
|
384
580
|
// A facade is about a handler, so this walks HANDLERS. It walked entities before,
|
|
385
581
|
// which made an entity a precondition for being callable at all: a handler naming
|
|
@@ -397,7 +593,7 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
397
593
|
const entity = frond.entities.find((e) => e.name === subject);
|
|
398
594
|
const facadeKey = facadeKeyOf(handler.address);
|
|
399
595
|
buildFacade(entity, handler, scope, facadeKey);
|
|
400
|
-
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as
|
|
596
|
+
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
|
|
401
597
|
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
402
598
|
}
|
|
403
599
|
|
|
@@ -423,27 +619,11 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
423
619
|
// key, which is what a Crud handler asks for, and under the port's own for a holder
|
|
424
620
|
// that legitimately names it. Registering only the latter left a named surface with
|
|
425
621
|
// no facade at all once the façade stopped spelling the storage.
|
|
426
|
-
if (entity
|
|
427
|
-
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
428
|
-
const outputSchema = handler.outputOverride ?? (handler.ctor as any).__output;
|
|
429
|
-
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
430
|
-
? baseStorage.output(outputSchema)
|
|
431
|
-
: baseStorage;
|
|
432
|
-
// The view is handed over so a filter on a field this facade hides is SAID. The
|
|
433
|
-
// guard holds no logger — a warning is the boot's to voice, as a seed's report is.
|
|
434
|
-
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name, {
|
|
435
|
-
...(outputSchema && outputSchema !== entity.entityClass
|
|
436
|
-
? { view: (outputSchema as { getFields(): Fields }).getFields() }
|
|
437
|
-
: {}),
|
|
438
|
-
outOfView: (message) => frondLog.warn(message),
|
|
439
|
-
}, heldBy(entity.entityClass, entity.name, assembly.hosting), releasing(assembly.hosting)).guard(scoped);
|
|
440
|
-
surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
|
|
441
|
-
surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
|
|
442
|
-
}
|
|
622
|
+
if (entity) registerSurfaceStorage(entity, handler, surfaceScope, assembly, frondLog);
|
|
443
623
|
|
|
444
624
|
const facadeKey = facadeKeyOf(handler.address, handler.surface);
|
|
445
625
|
buildFacade(entity, handler, surfaceScope, facadeKey);
|
|
446
|
-
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as
|
|
626
|
+
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
|
|
447
627
|
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
448
628
|
}
|
|
449
629
|
|