@fougere/core 0.17.4-alpha.0 → 0.19.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/README.md +1 -1
- package/dist/BootRefusal.d.ts +9 -0
- package/dist/BootRefusal.d.ts.map +1 -0
- package/dist/BootRefusal.js +12 -0
- package/dist/BootRefusal.js.map +1 -0
- package/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js +2 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/Declared.d.ts +2 -5
- package/dist/Declared.d.ts.map +1 -1
- package/dist/DeclaredHandler.d.ts +3 -8
- package/dist/DeclaredHandler.d.ts.map +1 -1
- package/dist/DeclaredSubject.d.ts +2 -5
- package/dist/DeclaredSubject.d.ts.map +1 -1
- package/dist/EffectiveOperation.d.ts +7 -6
- package/dist/EffectiveOperation.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +9 -6
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FrondConfig.d.ts +0 -2
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts +3 -8
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +2 -1
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/OperationOverride.d.ts +5 -3
- package/dist/OperationOverride.d.ts.map +1 -1
- package/dist/SourceView.d.ts +10 -8
- package/dist/SourceView.d.ts.map +1 -1
- package/dist/SourceView.js +10 -1
- package/dist/SourceView.js.map +1 -1
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +2 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts +8 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +16 -11
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +27 -15
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +1 -0
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +26 -14
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/unregistered.d.ts +4 -2
- package/dist/boot/unregistered.d.ts.map +1 -1
- package/dist/boot/unregistered.js +30 -30
- package/dist/boot/unregistered.js.map +1 -1
- package/dist/contract.d.ts +3 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +3 -0
- package/dist/contract.js.map +1 -1
- package/dist/descriptor/CollectorEntry.d.ts +2 -7
- package/dist/descriptor/CollectorEntry.d.ts.map +1 -1
- package/dist/descriptor/FrondDescriptor.d.ts +5 -23
- package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
- package/dist/descriptor/FrondDescriptor.js.map +1 -1
- package/dist/descriptor/HandlerEntry.d.ts +2 -7
- package/dist/descriptor/HandlerEntry.d.ts.map +1 -1
- package/dist/descriptor/MiddlewareEntry.d.ts +2 -7
- package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
- package/dist/descriptor/PresenterEntry.d.ts +2 -7
- package/dist/descriptor/PresenterEntry.d.ts.map +1 -1
- package/dist/descriptor/ProviderEntry.d.ts +2 -7
- package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
- package/dist/descriptor/Subject.d.ts +10 -0
- package/dist/descriptor/Subject.d.ts.map +1 -0
- package/dist/descriptor/Subject.js +2 -0
- package/dist/descriptor/Subject.js.map +1 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -0
- package/dist/descriptor/index.js.map +1 -1
- package/dist/diagnostic.d.ts +2 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +2 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts +13 -1
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +48 -2
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/EntityLookup.d.ts +4 -0
- package/dist/dispatch/EntityLookup.d.ts.map +1 -0
- package/dist/dispatch/EntityLookup.js +2 -0
- package/dist/dispatch/EntityLookup.js.map +1 -0
- package/dist/dispatch/Facade.d.ts +3 -0
- package/dist/dispatch/Facade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +2 -0
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +14 -4
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/entry/facade.d.ts +10 -1
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +26 -2
- package/dist/entry/facade.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/prefab/AggregateConstructor.d.ts +2 -2
- package/dist/prefab/AggregateConstructor.d.ts.map +1 -1
- package/dist/prefab/AggregateOf.d.ts +2 -2
- package/dist/prefab/AggregateOf.d.ts.map +1 -1
- package/dist/prefab/AggregateShape.d.ts +2 -2
- package/dist/prefab/AggregateShape.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts +2 -2
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +13 -10
- 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/CrudViews.d.ts +2 -2
- package/dist/prefab/CrudViews.d.ts.map +1 -1
- package/dist/prefab/Middleware.d.ts +13 -0
- package/dist/prefab/Middleware.d.ts.map +1 -0
- package/dist/prefab/Middleware.js +10 -0
- package/dist/prefab/Middleware.js.map +1 -0
- package/dist/prefab/MirrorConstructor.d.ts +2 -2
- package/dist/prefab/MirrorConstructor.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/RepositoryConstructor.d.ts +4 -4
- package/dist/prefab/RepositoryConstructor.d.ts.map +1 -1
- package/dist/prefab/RepositoryConstructor.js.map +1 -1
- package/dist/prefab/collector.d.ts +2 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/opsOf.d.ts +4 -0
- package/dist/prefab/opsOf.d.ts.map +1 -0
- package/dist/prefab/opsOf.js +5 -0
- package/dist/prefab/opsOf.js.map +1 -0
- package/dist/prefab/prefab.d.ts +3 -3
- package/dist/prefab/prefab.d.ts.map +1 -1
- package/dist/prefab/presenter.d.ts +3 -3
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/wire/Addresses.d.ts +4 -4
- package/dist/wire/Addresses.d.ts.map +1 -1
- package/dist/wire/Cardinality.d.ts +4 -0
- package/dist/wire/Cardinality.d.ts.map +1 -0
- package/dist/wire/Cardinality.js +3 -0
- package/dist/wire/Cardinality.js.map +1 -0
- package/dist/wire/Facade.d.ts +5 -7
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/Invocation.d.ts +2 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +4 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/InvocationContext.d.ts +6 -0
- package/dist/wire/InvocationContext.d.ts.map +1 -1
- package/dist/wire/OperationContract.d.ts +2 -1
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +2 -2
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/OperationKind.d.ts +3 -1
- package/dist/wire/OperationKind.d.ts.map +1 -1
- package/dist/wire/OperationKind.js +2 -1
- package/dist/wire/OperationKind.js.map +1 -1
- package/dist/wire/Placement.d.ts +4 -0
- package/dist/wire/Placement.d.ts.map +1 -0
- package/dist/wire/Placement.js +3 -0
- package/dist/wire/Placement.js.map +1 -0
- package/dist/wire/RouteKind.d.ts +2 -1
- package/dist/wire/RouteKind.d.ts.map +1 -1
- package/dist/wire/SignedCall.d.ts +2 -0
- package/dist/wire/SignedCall.d.ts.map +1 -1
- package/dist/wire/StateShape.d.ts +1 -2
- package/dist/wire/StateShape.d.ts.map +1 -1
- package/dist/wire/StateShape.js +11 -11
- package/dist/wire/StateShape.js.map +1 -1
- package/dist/wire/TypeRef.d.ts +7 -0
- package/dist/wire/TypeRef.d.ts.map +1 -1
- package/dist/wire/binding.d.ts +11 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +4 -2
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/CardOp.d.ts +4 -10
- package/dist/wire/card/CardOp.d.ts.map +1 -1
- package/dist/wire/refusals.d.ts +2 -1
- package/dist/wire/refusals.d.ts.map +1 -1
- package/dist/wire/refusals.js.map +1 -1
- package/dist/wire/topology/DeclaredFrond.d.ts +2 -1
- package/dist/wire/topology/DeclaredFrond.d.ts.map +1 -1
- package/dist/wire/topology/FrondPlacement.d.ts +2 -1
- package/dist/wire/topology/FrondPlacement.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/BootRefusal.ts +11 -0
- package/src/CallIdentity.ts +2 -1
- package/src/Declared.ts +3 -7
- package/src/DeclaredHandler.ts +3 -8
- package/src/DeclaredSubject.ts +3 -5
- package/src/EffectiveOperation.ts +7 -6
- package/src/EffectiveOperationModel.ts +11 -7
- package/src/FrondConfig.ts +0 -2
- package/src/FrondDeclaration.ts +5 -8
- package/src/OperationOverride.ts +5 -3
- package/src/SourceView.ts +21 -3
- package/src/boot/AppLifecycle.ts +2 -1
- package/src/boot/Emissions.ts +18 -11
- package/src/boot/bootstrap.ts +32 -16
- package/src/boot/card.ts +1 -2
- package/src/boot/install.ts +1 -0
- package/src/boot/ownership.ts +35 -16
- package/src/boot/unregistered.ts +31 -33
- package/src/contract.ts +3 -0
- package/src/descriptor/CollectorEntry.ts +2 -7
- package/src/descriptor/FrondDescriptor.ts +5 -19
- package/src/descriptor/HandlerEntry.ts +2 -7
- package/src/descriptor/MiddlewareEntry.ts +2 -7
- package/src/descriptor/PresenterEntry.ts +2 -7
- package/src/descriptor/ProviderEntry.ts +2 -7
- package/src/descriptor/Subject.ts +10 -0
- package/src/descriptor/index.ts +1 -0
- package/src/diagnostic.ts +4 -2
- package/src/dispatch/ArgumentResolver.ts +54 -2
- package/src/dispatch/EntityLookup.ts +4 -0
- package/src/dispatch/Facade.ts +3 -0
- package/src/dispatch/HandlerFacade.ts +14 -6
- package/src/entry/facade.ts +38 -2
- package/src/index.ts +5 -1
- package/src/prefab/AggregateConstructor.ts +2 -2
- package/src/prefab/AggregateOf.ts +2 -2
- package/src/prefab/AggregateShape.ts +2 -2
- package/src/prefab/CrudConstructor.ts +18 -13
- package/src/prefab/CrudOps.ts +3 -3
- package/src/prefab/CrudViews.ts +2 -2
- package/src/prefab/Middleware.ts +13 -0
- package/src/prefab/MirrorConstructor.ts +2 -2
- package/src/prefab/RepositoryConstructor.ts +9 -9
- package/src/prefab/collector.ts +4 -3
- package/src/prefab/opsOf.ts +6 -0
- package/src/prefab/prefab.ts +5 -5
- package/src/prefab/presenter.ts +3 -3
- package/src/wire/Addresses.ts +4 -4
- package/src/wire/Cardinality.ts +4 -0
- package/src/wire/Facade.ts +6 -9
- package/src/wire/Invocation.ts +3 -1
- package/src/wire/InvocationContext.ts +6 -0
- package/src/wire/OperationContract.ts +4 -3
- package/src/wire/OperationKind.ts +4 -1
- package/src/wire/Placement.ts +4 -0
- package/src/wire/RouteKind.ts +3 -1
- package/src/wire/SignedCall.ts +2 -0
- package/src/wire/StateShape.ts +9 -11
- package/src/wire/TypeRef.ts +4 -0
- package/src/wire/binding.ts +10 -2
- package/src/wire/card/CardOp.ts +4 -10
- package/src/wire/refusals.ts +2 -1
- package/src/wire/topology/DeclaredFrond.ts +3 -1
- package/src/wire/topology/FrondPlacement.ts +3 -1
package/src/boot/Emissions.ts
CHANGED
|
@@ -172,8 +172,16 @@ export class Emissions {
|
|
|
172
172
|
return (this.subscribers.get(fact) ?? []).map(({ facade }) => facade);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* What a listener is handed a fact through: the calls a door makes, never the facade code
|
|
177
|
+
* holds. A fact arrives as the invocation's input and the listener's binding plan places it,
|
|
178
|
+
* wherever it sits in the signature.
|
|
179
|
+
*/
|
|
180
|
+
private calls: (facade: string) => Record<string, Function> | undefined = () => undefined;
|
|
181
|
+
|
|
175
182
|
/** Register one emission value per fact — announced here, or merely listened to. */
|
|
176
|
-
register(): void {
|
|
183
|
+
register(calls: (facade: string) => Record<string, Function> | undefined): void {
|
|
184
|
+
this.calls = calls;
|
|
177
185
|
this.orderPipes();
|
|
178
186
|
for (const fact of new Set([...this.announced, ...this.subscribers.keys()])) {
|
|
179
187
|
this.container.registerValue(emitKeyOf(fact), (raw: unknown) => this.announce(fact, raw));
|
|
@@ -295,8 +303,7 @@ export class Emissions {
|
|
|
295
303
|
private async finished(fact: string, payload: unknown): Promise<unknown> {
|
|
296
304
|
let carried = payload;
|
|
297
305
|
for (const link of this.pipes.get(fact) ?? []) {
|
|
298
|
-
const
|
|
299
|
-
const answered = await facade[link.op]({ ...Invocation.empty, input: carried });
|
|
306
|
+
const answered = await this.reach(link.facade, fact)[link.op]({ ...Invocation.empty, input: carried });
|
|
300
307
|
|
|
301
308
|
// A link that answers nothing SUPPRESSES the fact — every subscriber was then handed
|
|
302
309
|
// `null` and crashed reading it, one message each. It is refused rather than named,
|
|
@@ -377,18 +384,18 @@ export class Emissions {
|
|
|
377
384
|
facade,
|
|
378
385
|
op,
|
|
379
386
|
done: ambient.enterChain(fact, async () => {
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
handler = this.container.resolve<Record<string, Function>>(facade);
|
|
383
|
-
} catch (cause) {
|
|
384
|
-
throw new Error(`${fact} → ${facade} could not be reached`, { cause });
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return handler[op]({ ...Invocation.empty, input: payload });
|
|
387
|
+
return this.reach(facade, fact)[op]({ ...Invocation.empty, input: payload });
|
|
388
388
|
}),
|
|
389
389
|
}));
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
private reach(facade: string, fact: string): Record<string, Function> {
|
|
393
|
+
const calls = this.calls(facade);
|
|
394
|
+
if (!calls) throw new Error(`${fact} → ${facade} could not be reached`);
|
|
395
|
+
|
|
396
|
+
return calls;
|
|
397
|
+
}
|
|
398
|
+
|
|
392
399
|
/** A subscriber refusing the SHAPE, said in one line instead of dumped as an error. */
|
|
393
400
|
private describeRefusal(fact: string, cause: unknown): string | undefined {
|
|
394
401
|
const refusals = validationErrorsOf(cause);
|
package/src/boot/bootstrap.ts
CHANGED
|
@@ -57,7 +57,8 @@ import { LocalRoutePolicy } from '../dispatch/LocalRoutePolicy.js';
|
|
|
57
57
|
import { OperationRoute } from '../dispatch/OperationRoute.js';
|
|
58
58
|
import { remoteRoutes } from '../dispatch/remoteRoutes.js';
|
|
59
59
|
import { RouteRegistry } from '../dispatch/RouteRegistry.js';
|
|
60
|
-
import { facadeOperations } from '../entry/facade.js';
|
|
60
|
+
import { callOperations, facadeOperations } from '../entry/facade.js';
|
|
61
|
+
import { decoded } from '../dispatch/decoded.js';
|
|
61
62
|
|
|
62
63
|
const LOG_LINE = lowerFirst(LogLine.name);
|
|
63
64
|
|
|
@@ -403,6 +404,7 @@ interface Serving {
|
|
|
403
404
|
container: Container;
|
|
404
405
|
fronds: Fronds;
|
|
405
406
|
remoteRouter: RemoteRouter | undefined;
|
|
407
|
+
dispatcher: Dispatcher;
|
|
406
408
|
localDispatcher: Dispatcher;
|
|
407
409
|
routeRegistry: RouteRegistry;
|
|
408
410
|
effectiveByKey: Map<string, EffectiveOperationsMap>;
|
|
@@ -415,7 +417,7 @@ interface Serving {
|
|
|
415
417
|
* `facadeFor` is THE rule, stated once: every projection reads it and nothing else.
|
|
416
418
|
*/
|
|
417
419
|
function readings(
|
|
418
|
-
{ container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log }: Serving,
|
|
420
|
+
{ container, fronds, remoteRouter, dispatcher, localDispatcher, routeRegistry, effectiveByKey, log }: Serving,
|
|
419
421
|
): Pick<App, 'resolve' | 'schemaFor' | 'facadeFor' | 'operationsFor' | 'presenterFor'> {
|
|
420
422
|
const resolve = <T>(name: string): T => {
|
|
421
423
|
try {
|
|
@@ -454,6 +456,19 @@ function readings(
|
|
|
454
456
|
}
|
|
455
457
|
};
|
|
456
458
|
|
|
459
|
+
/**
|
|
460
|
+
* What a door is handed for an address it may serve: the calls, not the facade code holds.
|
|
461
|
+
* The names are the routes this process registered; an address served elsewhere has none
|
|
462
|
+
* before it is reached, so its calls are named when they are made.
|
|
463
|
+
*/
|
|
464
|
+
const callsAt = (address: string, surface: string | undefined, through: Dispatcher): Record<string, Function> => {
|
|
465
|
+
const names = routeRegistry.operationNames(address, surface);
|
|
466
|
+
if (names.length === 0) return callOperations(through, address, undefined, surface);
|
|
467
|
+
const effective = effectiveByKey.get(facadeKeyOf(address, surface));
|
|
468
|
+
|
|
469
|
+
return callOperations(through, address, names, surface, (operation, answer) => decoded(effective?.get(operation)?.output, answer));
|
|
470
|
+
};
|
|
471
|
+
|
|
457
472
|
/** Said once per pair, so a facade that registers in a loop says it once. */
|
|
458
473
|
const saidAbsent = new Set<string>();
|
|
459
474
|
|
|
@@ -474,9 +489,9 @@ function readings(
|
|
|
474
489
|
|
|
475
490
|
/** THE membership rule, stated once — every projection reads this and nothing else. */
|
|
476
491
|
const facadeFor = (address: string, surface?: string): Record<string, Function> | undefined => {
|
|
477
|
-
if (!surface) return facadeAt(facadeKeyOf(address), true);
|
|
492
|
+
if (!surface) return facadeAt(facadeKeyOf(address), true) && callsAt(address, undefined, dispatcher);
|
|
478
493
|
|
|
479
|
-
const own = facadeAt(facadeKeyOf(address, surface), false);
|
|
494
|
+
const own = facadeAt(facadeKeyOf(address, surface), false) && callsAt(address, surface, localDispatcher);
|
|
480
495
|
if (!fronds.owner(address)) {
|
|
481
496
|
sayNoSurfaceAcross(address, surface);
|
|
482
497
|
|
|
@@ -490,7 +505,7 @@ function readings(
|
|
|
490
505
|
const fallback = facadeAt(facadeKeyOf(address), false);
|
|
491
506
|
|
|
492
507
|
return fallback
|
|
493
|
-
?
|
|
508
|
+
? callOperations(
|
|
494
509
|
localDispatcher,
|
|
495
510
|
address,
|
|
496
511
|
routeRegistry.operationNames(address, surface),
|
|
@@ -574,7 +589,7 @@ function answerRemotes(
|
|
|
574
589
|
{ container, routeRegistry, dispatcher, getMiddlewares, options, state }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares' | 'options' | 'state'>,
|
|
575
590
|
remoteRouter: RemoteRouter,
|
|
576
591
|
fronds: Fronds,
|
|
577
|
-
):
|
|
592
|
+
): (name: string) => boolean {
|
|
578
593
|
// Only for what another process may serve: an entity a frond hosts HERE and gives no handler
|
|
579
594
|
// is served by nobody, and a stand-in for it had GraphQL project an entity with no operation.
|
|
580
595
|
const servedElsewhere = (address: string): boolean => {
|
|
@@ -582,9 +597,8 @@ function answerRemotes(
|
|
|
582
597
|
|
|
583
598
|
return owner === undefined || owner.name in (options.remotes ?? {});
|
|
584
599
|
};
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
: undefined));
|
|
600
|
+
const answeredElsewhere = (name: string): boolean => isFacadeKey(name) && servedElsewhere(addressOf(name));
|
|
601
|
+
container.setFallback((name) => (answeredElsewhere(name) ? facadeOperations(dispatcher, addressOf(name)) : undefined));
|
|
588
602
|
|
|
589
603
|
const remoteFacades = new Map<string, Record<string, Function>>();
|
|
590
604
|
routeRegistry.addResolver(remoteRoutes((address) => {
|
|
@@ -595,6 +609,8 @@ function answerRemotes(
|
|
|
595
609
|
|
|
596
610
|
return facade;
|
|
597
611
|
}));
|
|
612
|
+
|
|
613
|
+
return answeredElsewhere;
|
|
598
614
|
}
|
|
599
615
|
|
|
600
616
|
/** Refused rather than replaced: two declarations of one name would make the answer depend on wiring order. */
|
|
@@ -695,16 +711,16 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
695
711
|
refuseWhatDoesNotHold(refused);
|
|
696
712
|
warnAboutUnboundPorts(options, boundPorts, log);
|
|
697
713
|
|
|
714
|
+
const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
|
|
715
|
+
container, fronds, remoteRouter, dispatcher, localDispatcher, routeRegistry, effectiveByKey, log,
|
|
716
|
+
});
|
|
717
|
+
|
|
698
718
|
// A pipe order and an `Emit<T, A>` are read here, once every facade exists.
|
|
699
|
-
emissions.register();
|
|
719
|
+
emissions.register((facade) => facadeFor(addressOf(facade)));
|
|
700
720
|
refuseWhatDoesNotHold(refused);
|
|
701
721
|
markLineCarriers(emissions);
|
|
702
722
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
|
|
706
|
-
container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
|
|
707
|
-
});
|
|
723
|
+
const answeredElsewhere = remoteRouter ? answerRemotes(assembly, remoteRouter, fronds) : () => false;
|
|
708
724
|
|
|
709
725
|
const app: App = {
|
|
710
726
|
container,
|
|
@@ -741,7 +757,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
741
757
|
|
|
742
758
|
built = app;
|
|
743
759
|
await appLifecycle.up(app);
|
|
744
|
-
refuseWhatDoesNotHold(unregistered(container, fronds));
|
|
760
|
+
refuseWhatDoesNotHold(unregistered(container, fronds, answeredElsewhere));
|
|
745
761
|
stopAnnouncing = announceLines(emissions, container, carry);
|
|
746
762
|
|
|
747
763
|
return app;
|
package/src/boot/card.ts
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
import { Card } from '@fougere/schema';
|
|
3
3
|
import type { App } from './App.js';
|
|
4
4
|
import { factsAnnouncedBy } from '../wire/Emit.js';
|
|
5
|
-
import type { InvocationContext } from '../wire/InvocationContext.js';
|
|
6
5
|
import { type CardOp } from '../wire/card/CardOp.js';
|
|
7
6
|
import { type IdentityCard } from '../wire/card/IdentityCard.js';
|
|
8
7
|
import { facadeKeyOf } from '../wire/Facade.js';
|
|
9
8
|
|
|
10
|
-
type AnyFacade = Record<string, (
|
|
9
|
+
type AnyFacade = Record<string, (...args: unknown[]) => Promise<unknown>>;
|
|
11
10
|
|
|
12
11
|
/** Serialize what the app hosts, for one audience. */
|
|
13
12
|
export function identityCardOf(app: App, surface?: string): IdentityCard {
|
package/src/boot/install.ts
CHANGED
|
@@ -434,6 +434,7 @@ function buildFacadeInto(
|
|
|
434
434
|
frond: frond.name,
|
|
435
435
|
handlers: frond.handlers,
|
|
436
436
|
operations: operationModel.forHandler(handler),
|
|
437
|
+
entities: (name) => assembly.entityByName.get(lowerFirst(name)),
|
|
437
438
|
collectors: collectorTypeNames,
|
|
438
439
|
presenter: presenterMap.get(handler.address),
|
|
439
440
|
presenterScope: scope,
|
package/src/boot/ownership.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type ProviderEntry } from '../descriptor/ProviderEntry.js';
|
|
|
4
4
|
import { inheritsCrud } from '../prefab/CrudConstructor.js';
|
|
5
5
|
import { targetOf } from '../prefab/prefab.js';
|
|
6
6
|
import { ownedBy, repositoryKeyOf } from '../prefab/RepositoryConstructor.js';
|
|
7
|
-
import { entityOfStorageKey } from '../storage/Storage.js';
|
|
7
|
+
import { entityOfStorageKey, membersOfTogetherKey } from '../storage/Storage.js';
|
|
8
8
|
import type { Diagnostic } from '../diagnostic.js';
|
|
9
9
|
|
|
10
10
|
/** One name, one file: two providers under one key and the second silently replaces the first. */
|
|
@@ -129,23 +129,16 @@ function refuseStorageReached(
|
|
|
129
129
|
const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
|
|
130
130
|
|
|
131
131
|
for (const dep of decl.deps) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (owner && owner !== decl.ctor.name) {
|
|
137
|
-
refused.push({
|
|
138
|
-
severity: 'blocking',
|
|
139
|
-
code: 'aggregate-storage-reached',
|
|
140
|
-
filePath: decl.filePath,
|
|
141
|
-
frond,
|
|
142
|
-
subject: `${decl.ctor.name}(${dep})`,
|
|
143
|
-
message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
|
|
144
|
-
+ `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
|
|
145
|
-
+ `constructor(private ${entity}: ${owner}) {}`,
|
|
146
|
-
});
|
|
132
|
+
const framed = membersOfTogetherKey(dep);
|
|
133
|
+
if (framed) {
|
|
134
|
+
const members = framed.entities.map(lowerFirst).filter(known);
|
|
135
|
+
for (const entity of members) reachesOwned(decl, dep, entity, frond, owners, refused);
|
|
147
136
|
continue;
|
|
148
137
|
}
|
|
138
|
+
|
|
139
|
+
const entity = entityOfStorageKey(dep, known);
|
|
140
|
+
if (!entity) continue;
|
|
141
|
+
if (reachesOwned(decl, dep, entity, frond, owners, refused)) continue;
|
|
149
142
|
if (allowed.includes(entity)) continue;
|
|
150
143
|
|
|
151
144
|
refused.push({
|
|
@@ -162,6 +155,32 @@ function refuseStorageReached(
|
|
|
162
155
|
}
|
|
163
156
|
}
|
|
164
157
|
|
|
158
|
+
/** Whether `dep` reaches an entity an aggregate other than `decl` owns — a frame as much as a storage. */
|
|
159
|
+
function reachesOwned(
|
|
160
|
+
decl: { ctor: { name: string }; filePath: string },
|
|
161
|
+
dep: string,
|
|
162
|
+
entity: string,
|
|
163
|
+
frond: string,
|
|
164
|
+
owners: Map<string, string>,
|
|
165
|
+
refused: Diagnostic[],
|
|
166
|
+
): boolean {
|
|
167
|
+
const owner = owners.get(entity);
|
|
168
|
+
if (!owner || owner === decl.ctor.name) return false;
|
|
169
|
+
|
|
170
|
+
refused.push({
|
|
171
|
+
severity: 'blocking',
|
|
172
|
+
code: 'aggregate-storage-reached',
|
|
173
|
+
filePath: decl.filePath,
|
|
174
|
+
frond,
|
|
175
|
+
subject: `${decl.ctor.name}(${dep})`,
|
|
176
|
+
message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
|
|
177
|
+
+ `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
|
|
178
|
+
+ `constructor(private ${entity}: ${owner}) {}`,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
export function storageInUserCode(
|
|
166
185
|
frond: FrondDescriptor,
|
|
167
186
|
owners: Map<string, string>,
|
package/src/boot/unregistered.ts
CHANGED
|
@@ -1,42 +1,40 @@
|
|
|
1
1
|
import type { Container } from '@fougere/container';
|
|
2
2
|
import type { Diagnostic } from '../diagnostic.js';
|
|
3
3
|
import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
|
|
4
|
-
|
|
5
|
-
type Declared = { frond: string; filePath: string };
|
|
6
|
-
|
|
7
|
-
/** Every class a frond hands the container, by identity — the one key a bundler cannot rename. */
|
|
8
|
-
function declaredBy(fronds: readonly FrondDescriptor[]): Map<Function, Declared> {
|
|
9
|
-
const declared = new Map<Function, Declared>();
|
|
10
|
-
for (const frond of fronds) {
|
|
11
|
-
const entries = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors, ...frond.middlewares];
|
|
12
|
-
for (const { ctor, filePath } of entries) declared.set(ctor, { frond: frond.name, filePath });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return declared;
|
|
16
|
-
}
|
|
4
|
+
import { SEAMS } from './ports.js';
|
|
17
5
|
|
|
18
6
|
/**
|
|
19
7
|
* What a constructor asks for and nothing where it is built answers — refused at boot rather than
|
|
20
|
-
* at its first call, once the extensions have risen, since `up` registers too.
|
|
8
|
+
* at its first call, once the extensions have risen, since `up` registers too. Read off what each
|
|
9
|
+
* class DECLARES, in the scope of the frond that declares it. A seam is answered by the link that
|
|
10
|
+
* wraps it, and a facade another process serves by the stand-in `answeredElsewhere` names.
|
|
21
11
|
*/
|
|
22
|
-
export function unregistered(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
12
|
+
export function unregistered(
|
|
13
|
+
container: Container,
|
|
14
|
+
fronds: readonly FrondDescriptor[],
|
|
15
|
+
answeredElsewhere: (name: string) => boolean,
|
|
16
|
+
): Diagnostic[] {
|
|
17
|
+
const refused = new Map<string, Diagnostic>();
|
|
18
|
+
for (const frond of fronds) {
|
|
19
|
+
if (!container.has(`frond:${frond.name}`)) continue;
|
|
20
|
+
const scope = container.resolve<Container>(`frond:${frond.name}`);
|
|
21
|
+
const declared = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors, ...frond.middlewares];
|
|
22
|
+
for (const { ctor, deps, filePath } of declared) {
|
|
23
|
+
for (const missing of deps) {
|
|
24
|
+
if (scope.has(missing) || SEAMS.has(missing) || answeredElsewhere(missing)) continue;
|
|
25
|
+
if (refused.has(`${ctor.name}→${missing}`)) continue;
|
|
26
|
+
refused.set(`${ctor.name}→${missing}`, {
|
|
27
|
+
severity: 'blocking',
|
|
28
|
+
code: 'dependency-unregistered',
|
|
29
|
+
frond: frond.name,
|
|
30
|
+
subject: ctor.name,
|
|
31
|
+
filePath,
|
|
32
|
+
message: `${ctor.name} asks for ${missing}, and nothing in frond '${frond.name}' or above it answers that name. `
|
|
33
|
+
+ `A class of that name in a convention directory, or one extending it, would.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
severity: 'blocking' as const,
|
|
34
|
-
code: 'dependency-unregistered',
|
|
35
|
-
...(where ? { frond: where.frond } : {}),
|
|
36
|
-
subject,
|
|
37
|
-
filePath: where?.filePath ?? '',
|
|
38
|
-
message: `${subject} asks for ${missing}, and nothing ${where ? `in frond '${where.frond}' or above it ` : ''}answers that name. `
|
|
39
|
-
+ `A class of that name in a convention directory, or one extending it, would.`,
|
|
40
|
-
}];
|
|
41
|
-
});
|
|
39
|
+
return [...refused.values()];
|
|
42
40
|
}
|
package/src/contract.ts
CHANGED
|
@@ -53,6 +53,9 @@ export type { DeclaredEdge } from './wire/topology/DeclaredEdge.js';
|
|
|
53
53
|
export type { DeclaredFrond } from './wire/topology/DeclaredFrond.js';
|
|
54
54
|
export type { DeclaredTopology } from './wire/topology/DeclaredTopology.js';
|
|
55
55
|
export type { Edge } from './wire/topology/Edge.js';
|
|
56
|
+
export { OPERATION_KINDS, type OperationKind } from './wire/OperationKind.js';
|
|
57
|
+
export { CARDINALITIES, type Cardinality } from './wire/Cardinality.js';
|
|
58
|
+
export { PLACEMENTS, type Placement } from './wire/Placement.js';
|
|
56
59
|
export type { FrondPlacement } from './wire/topology/FrondPlacement.js';
|
|
57
60
|
export type { TopologyReport } from './wire/topology/TopologyReport.js';
|
|
58
61
|
export { assertIdentityCard } from './wire/card/IdentityCard.js';
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import type { Subject } from './Subject.js';
|
|
1
2
|
/** A discovered collector (resolves handler input params from invocation context). */
|
|
2
|
-
export interface CollectorEntry {
|
|
3
|
+
export interface CollectorEntry extends Subject {
|
|
3
4
|
/** Registration key of the TYPE this collector resolves — 'user', 'ability'. */
|
|
4
5
|
typeName: string;
|
|
5
|
-
/** The collector class. */
|
|
6
|
-
ctor: new (...args: never[]) => unknown;
|
|
7
|
-
/** Constructor dependency type names (from AST scan). */
|
|
8
|
-
deps: string[];
|
|
9
|
-
/** Absolute file path (for debugging). */
|
|
10
|
-
filePath: string;
|
|
11
6
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { OperationContract } from '../wire/OperationContract.js';
|
|
2
|
+
import type { OperationOverride } from '../OperationOverride.js';
|
|
3
|
+
import type { FrondConfig } from '../FrondConfig.js';
|
|
2
4
|
import type { ProviderEntry } from './ProviderEntry.js';
|
|
3
5
|
import type { EntityEntry } from './EntityEntry.js';
|
|
4
6
|
import type { HandlerEntry } from './HandlerEntry.js';
|
|
@@ -15,7 +17,7 @@ import type { ExtensionEntry } from './ExtensionEntry.js';
|
|
|
15
17
|
*/
|
|
16
18
|
export const nameOf = (provider: ProviderEntry): string => provider.name ?? provider.ctor.name;
|
|
17
19
|
|
|
18
|
-
export interface FrondDescriptor {
|
|
20
|
+
export interface FrondDescriptor extends Pick<FrondConfig, 'pipes' | 'surfaces' | 'reads'> {
|
|
19
21
|
name: string;
|
|
20
22
|
source: FrondSource;
|
|
21
23
|
providers: ProviderEntry[];
|
|
@@ -41,26 +43,10 @@ export interface FrondDescriptor {
|
|
|
41
43
|
* flat, and a directory says nothing about who shares its code.
|
|
42
44
|
*/
|
|
43
45
|
extends?: string;
|
|
44
|
-
/** The ops that finish a fact, in order — see `FrondConfig.pipes`. */
|
|
45
|
-
pipes?: Record<string, string[]>;
|
|
46
|
-
/**
|
|
47
|
-
* Per-surface entity lists from frond.config.ts (e.g. { graphql: ['Post'], rest: ['Post',
|
|
48
|
-
* 'Author'] }).
|
|
49
|
-
*/
|
|
50
|
-
surfaces?: Record<string, string[]>;
|
|
51
|
-
/** Entities this frond may read across sources — see `FrondConfig.reads`. */
|
|
52
|
-
reads?: string[];
|
|
53
46
|
/** Per-operation overrides from frond.config.ts. */
|
|
54
|
-
operationsOverrides?: Record<string, OperationContract & {
|
|
55
|
-
|
|
56
|
-
/** Class name to resolve from DI (overrides default `{Entity}Handler` lookup). */
|
|
47
|
+
operationsOverrides?: Record<string, OperationContract & Omit<OperationOverride, 'handler'> & {
|
|
48
|
+
/** The handler CLASS of `frond.config.ts`, by the name DI resolves it under. */
|
|
57
49
|
handlerName?: string;
|
|
58
|
-
/** Method name on the resolved handler (defaults to op name). */
|
|
59
|
-
method?: string;
|
|
60
|
-
/** The GraphQL root field this op answers to — read by `adapter/graphql`. */
|
|
61
|
-
graphql?: string;
|
|
62
|
-
/** Where it answers over REST — read by `adapter/rest`, the dual of `graphql`. */
|
|
63
|
-
rest?: { method?: string; path?: string; status?: number };
|
|
64
50
|
}>;
|
|
65
51
|
}
|
|
66
52
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { Subject } from './Subject.js';
|
|
1
2
|
import type { SchemaView } from '@fougere/schema';
|
|
2
3
|
import type { OperationsMap } from '../wire/OperationsMap.js';
|
|
3
4
|
|
|
4
5
|
/** A discovered handler (controls what a service exposes). */
|
|
5
|
-
export interface HandlerEntry {
|
|
6
|
+
export interface HandlerEntry extends Subject {
|
|
6
7
|
/** Registration key (e.g. 'postHandler'). */
|
|
7
8
|
name: string;
|
|
8
9
|
/**
|
|
@@ -10,14 +11,8 @@ export interface HandlerEntry {
|
|
|
10
11
|
* `post`).
|
|
11
12
|
*/
|
|
12
13
|
address: string;
|
|
13
|
-
/** The handler class. */
|
|
14
|
-
ctor: new (...args: never[]) => unknown;
|
|
15
14
|
/** All operations with full signatures for binding. */
|
|
16
15
|
operations: OperationsMap;
|
|
17
|
-
/** Constructor dependency type names (from AST scan). */
|
|
18
|
-
deps: string[];
|
|
19
|
-
/** Absolute file path (for debugging). */
|
|
20
|
-
filePath: string;
|
|
21
16
|
/** Whether this handler is part of the frond's public contract. */
|
|
22
17
|
exposed?: boolean;
|
|
23
18
|
/** Output schema override — when Crud(Entity, Output), restricts storage output. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Subject } from './Subject.js';
|
|
1
2
|
/**
|
|
2
3
|
* A discovered middleware — a class declaring `around(ctx, next)`, which runs before and after
|
|
3
4
|
* every operation of its frond, and of the fronds that inherit from it.
|
|
@@ -5,13 +6,7 @@
|
|
|
5
6
|
* How far it reaches is not written here: it is where the frond sits in `FougereConfig.fronds`,
|
|
6
7
|
* and stating it twice would put one decision in two places.
|
|
7
8
|
*/
|
|
8
|
-
export interface MiddlewareEntry {
|
|
9
|
+
export interface MiddlewareEntry extends Subject {
|
|
9
10
|
/** Class name — the key it registers under in its frond's scope. */
|
|
10
11
|
name: string;
|
|
11
|
-
/** The middleware class. */
|
|
12
|
-
ctor: new (...args: never[]) => unknown;
|
|
13
|
-
/** Constructor dependency type names (from AST scan). */
|
|
14
|
-
deps: string[];
|
|
15
|
-
/** Absolute file path (for debugging). */
|
|
16
|
-
filePath: string;
|
|
17
12
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import type { Subject } from './Subject.js';
|
|
1
2
|
import type { PresenterViews } from '../prefab/presenter.js';
|
|
2
3
|
import type { PresenterFieldMeta } from './PresenterFieldMeta.js';
|
|
3
4
|
|
|
4
5
|
/** A discovered presenter (computed fields for an entity's output). */
|
|
5
|
-
export interface PresenterEntry {
|
|
6
|
+
export interface PresenterEntry extends Subject {
|
|
6
7
|
/** Entity name this presenter enriches (e.g. 'post' from PostPresenter). */
|
|
7
8
|
entityName: string;
|
|
8
|
-
/** The presenter class. */
|
|
9
|
-
ctor: new (...args: never[]) => unknown;
|
|
10
9
|
/** Computed field names (method names on prototype). */
|
|
11
10
|
fields: string[];
|
|
12
11
|
/** Per-field type metadata (inferred from source via parser). */
|
|
@@ -16,8 +15,4 @@ export interface PresenterEntry {
|
|
|
16
15
|
* items: [OrderItemView] })`).
|
|
17
16
|
*/
|
|
18
17
|
views?: PresenterViews;
|
|
19
|
-
/** Constructor dependency type names (from AST scan). */
|
|
20
|
-
deps: string[];
|
|
21
|
-
/** Absolute file path (for debugging). */
|
|
22
|
-
filePath: string;
|
|
23
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { Subject } from './Subject.js';
|
|
1
2
|
/** A discovered provider — a class under `services/` or `repositories/`, injected by type. */
|
|
2
|
-
export interface ProviderEntry {
|
|
3
|
+
export interface ProviderEntry extends Subject {
|
|
3
4
|
/**
|
|
4
5
|
* The container key — the class name as the SOURCE spells it.
|
|
5
6
|
*
|
|
@@ -9,12 +10,6 @@ export interface ProviderEntry {
|
|
|
9
10
|
* A name a tool may rewrite is written down, for the same reason `deps` is.
|
|
10
11
|
*/
|
|
11
12
|
name?: string;
|
|
12
|
-
/** The class constructor (default export of the file). */
|
|
13
|
-
ctor: new (...args: never[]) => unknown;
|
|
14
|
-
/** Constructor dependency type names (from AST scan). */
|
|
15
|
-
deps: string[];
|
|
16
|
-
/** Absolute file path (for debugging). */
|
|
17
|
-
filePath: string;
|
|
18
13
|
/**
|
|
19
14
|
* What the source said and the runtime erased: a port, which a class below answers for and
|
|
20
15
|
* nothing may instantiate — never a candidate for the ports above it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Ctor } from '../Ctor.js';
|
|
2
|
+
|
|
3
|
+
/** A class the boot builds — what its constructor asks for, and where the scan found it. */
|
|
4
|
+
export interface Subject {
|
|
5
|
+
ctor: Ctor;
|
|
6
|
+
/** Constructor dependency type names (from AST scan). */
|
|
7
|
+
deps: string[];
|
|
8
|
+
/** Absolute file path (for debugging). */
|
|
9
|
+
filePath: string;
|
|
10
|
+
}
|
package/src/descriptor/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { type OperationsMap } from '../wire/OperationsMap.js';
|
|
|
21
21
|
export { type Param } from '../wire/Param.js';
|
|
22
22
|
export { type TypeRef } from '../wire/TypeRef.js';
|
|
23
23
|
export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
|
|
24
|
+
export { opsOf } from '../prefab/opsOf.js';
|
|
24
25
|
export type { Signature } from '../wire/Signature.js';
|
|
25
26
|
export { awaitKeyOf, emitKeyOf } from '../wire/Emit.js';
|
|
26
27
|
export { addressOf } from '../wire/Facade.js';
|
package/src/diagnostic.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BootRefusal } from './BootRefusal.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* One thing that does not hold, in the terms of whoever has to fix it — raised by the scan,
|
|
3
5
|
* by a boot rule, or by `fougere check`, which is why it belongs to no phase.
|
|
@@ -30,12 +32,12 @@ export interface Diagnostic {
|
|
|
30
32
|
* Every blocking diagnostic in ONE refusal, so a boot names all of them rather than the
|
|
31
33
|
* first — the shape `fougere check` renders, read back by whoever has to fix it.
|
|
32
34
|
*/
|
|
33
|
-
export function refusalOf(diagnostics: readonly Diagnostic[], what: string):
|
|
35
|
+
export function refusalOf(diagnostics: readonly Diagnostic[], what: string): BootRefusal | undefined {
|
|
34
36
|
const blocking = diagnostics.filter((one) => one.severity === 'blocking');
|
|
35
37
|
if (blocking.length === 0) return undefined;
|
|
36
38
|
|
|
37
39
|
const lines = blocking.map((one) =>
|
|
38
40
|
` [${one.code}]${one.subject ? ` ${one.subject}` : ''}\n ${one.message}\n ${one.filePath}`);
|
|
39
41
|
|
|
40
|
-
return new
|
|
42
|
+
return new BootRefusal(`Fougere boot refused: ${blocking.length} ${what}:\n${lines.join('\n')}`);
|
|
41
43
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { FieldValueValidator, type SchemaView } from '@fougere/schema';
|
|
1
2
|
import type { InvocationContext } from '../wire/InvocationContext.js';
|
|
2
3
|
import type { BindingPlan } from '../wire/binding.js';
|
|
3
4
|
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
4
5
|
import { FougereError } from '../wire/FougereError.js';
|
|
5
6
|
import type { CollectorLookup } from './CollectorLookup.js';
|
|
7
|
+
import type { EntityLookup } from './EntityLookup.js';
|
|
6
8
|
import { collectedAs } from '../prefab/collector.js';
|
|
9
|
+
import { validateInput } from './validateInput.js';
|
|
7
10
|
|
|
8
11
|
type ParamSource = Extract<BindingPlan[number]['source'], { kind: 'param' }>;
|
|
9
12
|
|
|
10
13
|
/** Resolves an operation's declared binding plan against one invocation. */
|
|
11
14
|
export class ArgumentResolver {
|
|
12
|
-
constructor(
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly collectors?: CollectorLookup,
|
|
17
|
+
private readonly entities?: EntityLookup,
|
|
18
|
+
) {}
|
|
13
19
|
|
|
14
20
|
async resolve(plan: BindingPlan, ctx: InvocationContext): Promise<unknown[]> {
|
|
15
21
|
const args: unknown[] = [];
|
|
@@ -26,7 +32,7 @@ export class ArgumentResolver {
|
|
|
26
32
|
break;
|
|
27
33
|
}
|
|
28
34
|
case 'param': {
|
|
29
|
-
args.push(ArgumentResolver.param(binding.source, binding.optional, ctx));
|
|
35
|
+
args.push(this.judged(binding.source, ArgumentResolver.param(binding.source, binding.optional, ctx)));
|
|
30
36
|
break;
|
|
31
37
|
}
|
|
32
38
|
case 'fact':
|
|
@@ -56,6 +62,39 @@ export class ArgumentResolver {
|
|
|
56
62
|
return args;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The arguments code wrote, judged where a door would have judged them: the input against
|
|
67
|
+
* the op's view, a value a collector would have produced through that collector's entity —
|
|
68
|
+
* which also puts back a `Date` that crossed a wire as text. Nothing is collected: what the
|
|
69
|
+
* caller left out stays out.
|
|
70
|
+
*/
|
|
71
|
+
given(plan: BindingPlan, input: SchemaView | undefined, ctx: InvocationContext): unknown[] {
|
|
72
|
+
const args = ctx.args ?? [];
|
|
73
|
+
|
|
74
|
+
return plan.map((binding, index) => {
|
|
75
|
+
const value = args[index];
|
|
76
|
+
switch (binding.source.kind) {
|
|
77
|
+
case 'context':
|
|
78
|
+
return ctx;
|
|
79
|
+
case 'input':
|
|
80
|
+
case 'fact':
|
|
81
|
+
case 'pipe':
|
|
82
|
+
return validateInput(input, { ...ctx, input: value }).input;
|
|
83
|
+
case 'collector': {
|
|
84
|
+
const collector = this.collectors?.(binding.source.typeName);
|
|
85
|
+
|
|
86
|
+
return collector ? collectedAs(collector, value) : value;
|
|
87
|
+
}
|
|
88
|
+
case 'param':
|
|
89
|
+
if (value === undefined && !binding.optional) throw refused(binding.source.name, 'Required');
|
|
90
|
+
|
|
91
|
+
return this.judged(binding.source, value);
|
|
92
|
+
case 'query':
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
/**
|
|
60
99
|
* A value the caller typed in a path or a query string. `null` is a value, not a miss: only
|
|
61
100
|
* `undefined` means absent, so `T | null` stays apart from `T | undefined`.
|
|
@@ -77,6 +116,19 @@ export class ArgumentResolver {
|
|
|
77
116
|
return coerced;
|
|
78
117
|
}
|
|
79
118
|
|
|
119
|
+
/** `age: Sample['age']` obeys what `Sample.age` declares, after the query string was read. */
|
|
120
|
+
private judged(source: ParamSource, value: unknown): unknown {
|
|
121
|
+
if (value === undefined || !source.field) return value;
|
|
122
|
+
|
|
123
|
+
const field = this.entities?.(source.field.entity)?.getFields()[source.field.name];
|
|
124
|
+
if (!field) return value;
|
|
125
|
+
|
|
126
|
+
const verdict = FieldValueValidator.of(field).parse(value);
|
|
127
|
+
if ('message' in verdict) throw refused(source.name, verdict.message);
|
|
128
|
+
|
|
129
|
+
return verdict.value;
|
|
130
|
+
}
|
|
131
|
+
|
|
80
132
|
private static number(value: unknown): number | undefined {
|
|
81
133
|
if (typeof value === 'number') return Number.isNaN(value) ? undefined : value;
|
|
82
134
|
if (typeof value !== 'string' || value.trim() === '') return undefined;
|