@fougere/core 0.17.3-alpha.0 → 0.18.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/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/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +6 -5
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +8 -4
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/unregistered.d.ts +11 -0
- package/dist/boot/unregistered.d.ts.map +1 -0
- package/dist/boot/unregistered.js +35 -0
- package/dist/boot/unregistered.js.map +1 -0
- 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/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +12 -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/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/OperationContract.d.ts +2 -1
- package/dist/wire/OperationContract.d.ts.map +1 -1
- 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/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 +5 -0
- package/dist/wire/TypeRef.d.ts.map +1 -1
- package/dist/wire/binding.d.ts +4 -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/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/bootstrap.ts +8 -5
- package/src/boot/install.ts +7 -4
- package/src/boot/unregistered.ts +40 -0
- 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/index.ts +3 -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 +17 -13
- package/src/prefab/CrudOps.ts +3 -3
- package/src/prefab/CrudViews.ts +2 -2
- 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/OperationContract.ts +2 -1
- package/src/wire/OperationKind.ts +4 -1
- package/src/wire/Placement.ts +4 -0
- package/src/wire/RouteKind.ts +3 -1
- package/src/wire/StateShape.ts +9 -11
- package/src/wire/TypeRef.ts +2 -0
- package/src/wire/binding.ts +7 -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/bootstrap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createContainer, type Container } from '@fougere/container';
|
|
2
|
+
import { unregistered } from './unregistered.js';
|
|
2
3
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
3
4
|
|
|
4
5
|
import type { HandlerEntry } from '../descriptor/HandlerEntry.js';
|
|
@@ -573,7 +574,7 @@ function answerRemotes(
|
|
|
573
574
|
{ container, routeRegistry, dispatcher, getMiddlewares, options, state }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares' | 'options' | 'state'>,
|
|
574
575
|
remoteRouter: RemoteRouter,
|
|
575
576
|
fronds: Fronds,
|
|
576
|
-
):
|
|
577
|
+
): (name: string) => boolean {
|
|
577
578
|
// Only for what another process may serve: an entity a frond hosts HERE and gives no handler
|
|
578
579
|
// is served by nobody, and a stand-in for it had GraphQL project an entity with no operation.
|
|
579
580
|
const servedElsewhere = (address: string): boolean => {
|
|
@@ -581,9 +582,8 @@ function answerRemotes(
|
|
|
581
582
|
|
|
582
583
|
return owner === undefined || owner.name in (options.remotes ?? {});
|
|
583
584
|
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
: undefined));
|
|
585
|
+
const answeredElsewhere = (name: string): boolean => isFacadeKey(name) && servedElsewhere(addressOf(name));
|
|
586
|
+
container.setFallback((name) => (answeredElsewhere(name) ? facadeOperations(dispatcher, addressOf(name)) : undefined));
|
|
587
587
|
|
|
588
588
|
const remoteFacades = new Map<string, Record<string, Function>>();
|
|
589
589
|
routeRegistry.addResolver(remoteRoutes((address) => {
|
|
@@ -594,6 +594,8 @@ function answerRemotes(
|
|
|
594
594
|
|
|
595
595
|
return facade;
|
|
596
596
|
}));
|
|
597
|
+
|
|
598
|
+
return answeredElsewhere;
|
|
597
599
|
}
|
|
598
600
|
|
|
599
601
|
/** Refused rather than replaced: two declarations of one name would make the answer depend on wiring order. */
|
|
@@ -699,7 +701,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
699
701
|
refuseWhatDoesNotHold(refused);
|
|
700
702
|
markLineCarriers(emissions);
|
|
701
703
|
|
|
702
|
-
|
|
704
|
+
const answeredElsewhere = remoteRouter ? answerRemotes(assembly, remoteRouter, fronds) : () => false;
|
|
703
705
|
|
|
704
706
|
const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
|
|
705
707
|
container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
|
|
@@ -740,6 +742,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
740
742
|
|
|
741
743
|
built = app;
|
|
742
744
|
await appLifecycle.up(app);
|
|
745
|
+
refuseWhatDoesNotHold(unregistered(container, fronds, answeredElsewhere));
|
|
743
746
|
stopAnnouncing = announceLines(emissions, container, carry);
|
|
744
747
|
|
|
745
748
|
return app;
|
package/src/boot/install.ts
CHANGED
|
@@ -285,13 +285,16 @@ function registerProviders(
|
|
|
285
285
|
ownLogger: OwnLogger,
|
|
286
286
|
frondLog: Logger,
|
|
287
287
|
): Map<string, ProviderEntry[]> {
|
|
288
|
-
for (const provider of frond.providers) {
|
|
289
|
-
scope.register(nameOf(provider), provider.ctor, { deps: ownLogger(nameOf(provider), provider.deps), ...lifetimeOf(provider.ctor) });
|
|
290
|
-
}
|
|
291
288
|
// What this frond puts in front of one of the framework's own ports. Its own, like every
|
|
292
289
|
// provider — a link goes where its frond goes, which is what a frond behind `remotes:`
|
|
293
|
-
// takes with it.
|
|
290
|
+
// takes with it. A link is built by `wrapping`, never by the container: it asks for the seam,
|
|
291
|
+
// and nothing answers that name.
|
|
294
292
|
const seams = seamChains(frond.providers, ports, refused);
|
|
293
|
+
const links = new Set([...seams.values()].flat());
|
|
294
|
+
for (const provider of frond.providers) {
|
|
295
|
+
if (links.has(provider)) continue;
|
|
296
|
+
scope.register(nameOf(provider), provider.ctor, { deps: ownLogger(nameOf(provider), provider.deps), ...lifetimeOf(provider.ctor) });
|
|
297
|
+
}
|
|
295
298
|
for (const [seam, links] of seams) {
|
|
296
299
|
boundPorts.add(seam);
|
|
297
300
|
frondLog.debug(`seam ${seam} → ${links.map((one) => one.ctor.name).join(' → ')} → the realization`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Container } from '@fougere/container';
|
|
2
|
+
import type { Diagnostic } from '../diagnostic.js';
|
|
3
|
+
import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
|
|
4
|
+
import { SEAMS } from './ports.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a constructor asks for and nothing where it is built answers — refused at boot rather than
|
|
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.
|
|
11
|
+
*/
|
|
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
|
+
}
|
|
38
|
+
|
|
39
|
+
return [...refused.values()];
|
|
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/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ export type { AppMiddleware } from './wire/AppMiddleware.js';
|
|
|
63
63
|
export type { AppNext } from './wire/AppNext.js';
|
|
64
64
|
export type { OperationContext } from './wire/OperationContext.js';
|
|
65
65
|
export { assertIdentityCard, RPC_ADDRESS, ENVELOPE_BYTES, maxBodyBytes, maxFrameBytes, setMaxBodyBytes } from './contract.js';
|
|
66
|
+
export { OPERATION_KINDS, CARDINALITIES, PLACEMENTS, type OperationKind, type Cardinality, type Placement } from './contract.js';
|
|
66
67
|
export { createLocalRunner, createAppRunner } from './boot/runner.js';
|
|
67
68
|
|
|
68
69
|
// A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
|
|
@@ -81,6 +82,7 @@ export { declaredTopologyOf } from './boot/declared.js';
|
|
|
81
82
|
// Both halves of what a call can refuse, put together where they are READ — the framework's
|
|
82
83
|
// follows from `kind` and `input`, so only the frond's travels.
|
|
83
84
|
export { refusalsOf, type Refusable } from './wire/refusals.js';
|
|
85
|
+
export type { Binding } from './Binding.js';
|
|
84
86
|
// The facades as TYPES — empty here, filled by the `.d.ts` the scan writes beside the app.
|
|
85
87
|
export type { Addresses } from './wire/Addresses.js';
|
|
86
88
|
export type { Answer } from './wire/Answer.js';
|
|
@@ -129,7 +131,7 @@ export { Storage, togetherKeyOf } from './storage/Storage.js';
|
|
|
129
131
|
export type { Constraint } from './Constraint.js';
|
|
130
132
|
export type { Source } from './Source.js';
|
|
131
133
|
export type { SourceConfig } from './SourceConfig.js';
|
|
132
|
-
export type
|
|
134
|
+
export { sourceViewOf, type SourceView } from './SourceView.js';
|
|
133
135
|
export { Sources } from './Source.js';
|
|
134
136
|
export { storageOver } from './storage/Store.js';
|
|
135
137
|
export type { Store } from './storage/Store.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AggregateShape } from './AggregateShape.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type Entity } from '@fougere/schema';
|
|
3
3
|
|
|
4
|
-
export type AggregateConstructor<E extends readonly
|
|
4
|
+
export type AggregateConstructor<E extends readonly Entity[]> =
|
|
5
5
|
(new (...storages: unknown[]) => AggregateShape<E>) & {
|
|
6
6
|
readonly __entity: unknown;
|
|
7
7
|
/** The entities this class owns. Present from two on — an owner of one owns nothing. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Entity } from '@fougere/schema';
|
|
2
2
|
import type { Storage } from '../storage/Storage.js';
|
|
3
3
|
|
|
4
4
|
/** The tuple an aggregate holds — the readable form of {@link AggregateShape}'s member. */
|
|
5
|
-
export type AggregateOf<E extends readonly
|
|
5
|
+
export type AggregateOf<E extends readonly Entity[]> =
|
|
6
6
|
{ [K in keyof E]: Storage<InstanceType<E[K]>> };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Storage } from '../storage/Storage.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Entity } from '@fougere/schema';
|
|
3
3
|
|
|
4
4
|
/** What an aggregate's subclass sees. */
|
|
5
|
-
export declare abstract class AggregateShape<E extends readonly
|
|
5
|
+
export declare abstract class AggregateShape<E extends readonly Entity[]> {
|
|
6
6
|
protected storages: { [K in keyof E]: Storage<InstanceType<E[K]>> };
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lowerFirst, type
|
|
1
|
+
import { FieldSet, lowerFirst, type Entity, type SchemaView } from '@fougere/schema';
|
|
2
2
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
3
3
|
import type { Storage } from '../storage/Storage.js';
|
|
4
4
|
import type { OperationContract } from '../wire/OperationContract.js';
|
|
@@ -29,13 +29,15 @@ export function subjectOf(ctor: unknown, address: string): string {
|
|
|
29
29
|
return target?.name ? lowerFirst(target.name) : address;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** The id of the row an op acts on — a route
|
|
33
|
-
const byId =
|
|
32
|
+
/** The id of the row an op acts on — `id: Post['id']`, which a route places before the op. */
|
|
33
|
+
const byId = (entity: string) =>
|
|
34
|
+
({ name: 'id', source: { kind: 'param' as const, name: 'id', identifies: entity }, optional: false });
|
|
34
35
|
|
|
35
36
|
const fromBody = { name: 'input', source: { kind: 'input' as const }, optional: false };
|
|
36
37
|
|
|
37
38
|
/** The same five, written as the scan would have written them. */
|
|
38
|
-
const idParam =
|
|
39
|
+
const idParam = (entity: string, field: string) =>
|
|
40
|
+
({ name: 'id', type: { raw: `${entity}['${field}']`, name: 'string', identifies: { entity, field } } });
|
|
39
41
|
|
|
40
42
|
const inputParam = (entity: string) => ({ name: 'input', type: { raw: `Partial<${entity}>`, name: entity } });
|
|
41
43
|
|
|
@@ -50,6 +52,8 @@ const returns = (raw: string, name: string, extra?: { array?: boolean; nullable?
|
|
|
50
52
|
function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<string, OperationContract> {
|
|
51
53
|
const name = (entity as { name?: string }).name ?? 'Entity';
|
|
52
54
|
const input = inputParam(name);
|
|
55
|
+
const id = byId(name);
|
|
56
|
+
const idSignature = idParam(name, FieldSet.of(entity.getFields()).primary ?? 'id');
|
|
53
57
|
|
|
54
58
|
return {
|
|
55
59
|
list: {
|
|
@@ -61,8 +65,8 @@ function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<st
|
|
|
61
65
|
},
|
|
62
66
|
},
|
|
63
67
|
findById: {
|
|
64
|
-
output: entity, cardinality: 'maybe', binding: [
|
|
65
|
-
signature: { name: 'findById', returnType: returns(`${name} | undefined`, name, { nullable: true }), params: [
|
|
68
|
+
output: entity, cardinality: 'maybe', binding: [id],
|
|
69
|
+
signature: { name: 'findById', returnType: returns(`${name} | undefined`, name, { nullable: true }), params: [idSignature] },
|
|
66
70
|
},
|
|
67
71
|
create: {
|
|
68
72
|
input: entity, output: entity, cardinality: 'one', binding: [fromBody],
|
|
@@ -71,12 +75,12 @@ function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<st
|
|
|
71
75
|
// The patch view carries its own mode: an absent field is untouched, an
|
|
72
76
|
// immutable one re-supplied is refused.
|
|
73
77
|
update: {
|
|
74
|
-
input: entity.partial?.(), output: entity, cardinality: 'one', binding: [
|
|
75
|
-
signature: { name: 'update', returnType: returns(name, name), params: [
|
|
78
|
+
input: entity.partial?.(), output: entity, cardinality: 'one', binding: [id, fromBody],
|
|
79
|
+
signature: { name: 'update', returnType: returns(name, name), params: [idSignature, input] },
|
|
76
80
|
},
|
|
77
81
|
delete: {
|
|
78
|
-
cardinality: 'none', binding: [
|
|
79
|
-
signature: { name: 'delete', returnType: returns('boolean', 'boolean'), params: [
|
|
82
|
+
cardinality: 'none', binding: [id],
|
|
83
|
+
signature: { name: 'delete', returnType: returns('boolean', 'boolean'), params: [idSignature] },
|
|
80
84
|
},
|
|
81
85
|
};
|
|
82
86
|
}
|
|
@@ -103,7 +107,7 @@ export interface CrudConstructor<T, V = {}> {
|
|
|
103
107
|
*
|
|
104
108
|
* Documented: [handlers](https://fougere.dev/docs/business/handlers).
|
|
105
109
|
*/
|
|
106
|
-
export function Crud<E extends
|
|
110
|
+
export function Crud<E extends Entity, V extends CrudViews | Entity = {}>(
|
|
107
111
|
entity: E,
|
|
108
112
|
output?: V,
|
|
109
113
|
): CrudConstructor<InstanceType<E>, V> {
|
|
@@ -112,7 +116,7 @@ export function Crud<E extends EntityConstructor, V extends CrudViews | EntityCo
|
|
|
112
116
|
|
|
113
117
|
// A view is a class (it carries fields) ; a map of views is a plain object.
|
|
114
118
|
const perOp = typeof output === 'object' && output !== null ? (output as CrudViews) : undefined;
|
|
115
|
-
const wholeHandler = typeof output === 'function' ? (output as
|
|
119
|
+
const wholeHandler = typeof output === 'function' ? (output as Entity) : undefined;
|
|
116
120
|
|
|
117
121
|
return asCrudConstructor<T, V>(class CrudHandler {
|
|
118
122
|
static __entity = entity;
|
|
@@ -123,7 +127,7 @@ export function Crud<E extends EntityConstructor, V extends CrudViews | EntityCo
|
|
|
123
127
|
* What this prefab handler declares — read by the façade, merged under the author's own
|
|
124
128
|
* methods.
|
|
125
129
|
*/
|
|
126
|
-
static __ops: Record<string, OperationContract> = crudOps(entity
|
|
130
|
+
static __ops: Record<string, OperationContract> = crudOps(entity);
|
|
127
131
|
|
|
128
132
|
// Private: the five ops are how a subclass reaches the rows — `super.findById(id)` — and a
|
|
129
133
|
// query worth a name belongs to the repository, which it asks for like any handler does.
|
package/src/prefab/CrudOps.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CrudOpName } from './CrudOpName.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Entity } from '@fougere/schema';
|
|
3
3
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
4
4
|
import type { Page } from '../wire/Page.js';
|
|
5
5
|
|
|
@@ -8,8 +8,8 @@ type OutOf<V, K extends CrudOpName, T> =
|
|
|
8
8
|
// Bracketed on purpose: a naked `V extends …` DISTRIBUTES, and the no-view default
|
|
9
9
|
// is the empty map, whose `keyof` is `never` — distribution would then collapse
|
|
10
10
|
// every op's output to `never` instead of falling through to the entity.
|
|
11
|
-
[V] extends [
|
|
12
|
-
: K extends keyof V ? (V[K] extends
|
|
11
|
+
[V] extends [Entity] ? InstanceType<V & Entity>
|
|
12
|
+
: K extends keyof V ? (V[K] extends Entity ? InstanceType<V[K]> : T)
|
|
13
13
|
: T;
|
|
14
14
|
|
|
15
15
|
/** The five ops, typed from the entity and its views. */
|
package/src/prefab/CrudViews.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Entity } from '@fougere/schema';
|
|
2
2
|
import type { CrudOpName } from './CrudOpName.js';
|
|
3
3
|
|
|
4
4
|
/** Which view each op speaks — omitted ops speak the entity, the trivial view. */
|
|
5
|
-
export type CrudViews = Partial<Record<CrudOpName,
|
|
5
|
+
export type CrudViews = Partial<Record<CrudOpName, Entity>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Entity } from '@fougere/schema';
|
|
2
2
|
import type { Storage } from '../storage/Storage.js';
|
|
3
3
|
import type { Refreshed } from './Refreshed.js';
|
|
4
4
|
import type { MirrorOf } from './MirrorOf.js';
|
|
@@ -8,7 +8,7 @@ export interface MirrorConstructor<T> {
|
|
|
8
8
|
readonly __entity: unknown;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function Mirror<E extends
|
|
11
|
+
export function Mirror<E extends Entity>(shape: E): MirrorConstructor<InstanceType<E>> {
|
|
12
12
|
type T = InstanceType<E>;
|
|
13
13
|
|
|
14
14
|
abstract class MirrorBase implements MirrorOf<T> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { upperFirst, type
|
|
1
|
+
import { upperFirst, type Entity } from '@fougere/schema';
|
|
2
2
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
3
3
|
import type { ListResult } from '../storage/ListResult.js';
|
|
4
4
|
import type { SelectOption } from '../storage/SelectOption.js';
|
|
@@ -14,22 +14,22 @@ export interface RepositoryConstructor<T> {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** What an aggregate's subclass sees. */
|
|
17
|
-
declare abstract class AggregateShape<E extends readonly
|
|
17
|
+
declare abstract class AggregateShape<E extends readonly Entity[]> {
|
|
18
18
|
protected storages: { [K in keyof E]: Storage<InstanceType<E[K]>> };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function Repository<E extends
|
|
21
|
+
export function Repository<E extends Entity>(entity: E): RepositoryConstructor<InstanceType<E>>;
|
|
22
22
|
|
|
23
|
-
export function Repository<E extends readonly [
|
|
23
|
+
export function Repository<E extends readonly [Entity, Entity, ...Entity[]]>(
|
|
24
24
|
...entities: E
|
|
25
25
|
): AggregateConstructor<E>;
|
|
26
26
|
|
|
27
|
-
export function Repository(...entities:
|
|
27
|
+
export function Repository(...entities: Entity[]): unknown {
|
|
28
28
|
return entities.length === 1 ? one(entities[0]) : many(entities);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** The port forwarded, member by member. */
|
|
32
|
-
function one<E extends
|
|
32
|
+
function one<E extends Entity>(entity: E): RepositoryConstructor<InstanceType<E>> {
|
|
33
33
|
type T = InstanceType<E>;
|
|
34
34
|
|
|
35
35
|
class RepositoryBase implements Storage<T> {
|
|
@@ -56,7 +56,7 @@ function one<E extends EntityConstructor>(entity: E): RepositoryConstructor<Inst
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** The aggregate. */
|
|
59
|
-
function many<E extends readonly
|
|
59
|
+
function many<E extends readonly Entity[]>(entities: E): AggregateConstructor<E> {
|
|
60
60
|
class AggregateBase {
|
|
61
61
|
static readonly __entity = entities[0];
|
|
62
62
|
static readonly __owns = entities;
|
|
@@ -77,6 +77,6 @@ export function repositoryKeyOf(entity: string): string {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** The entities a class owns — empty for anything that is not an aggregate. */
|
|
80
|
-
export function ownedBy(ctor: unknown): readonly
|
|
81
|
-
return (ctor as { __owns?: readonly
|
|
80
|
+
export function ownedBy(ctor: unknown): readonly Entity[] {
|
|
81
|
+
return (ctor as { __owns?: readonly Entity[] } | undefined)?.__owns ?? [];
|
|
82
82
|
}
|
package/src/prefab/collector.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Documented: [collectors](https://fougere.dev/docs/business/collectors).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import type { AbstractCtor } from '../Ctor.js';
|
|
8
|
+
import { FieldValueValidator, dotted, json, upperFirst, type Entity, type Field, type SchemaView } from '@fougere/schema';
|
|
8
9
|
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
9
10
|
import { FougereError } from '../wire/FougereError.js';
|
|
10
11
|
|
|
11
|
-
export function Collector<T extends
|
|
12
|
+
export function Collector<T extends AbstractCtor>(target: T) {
|
|
12
13
|
class CollectorBase {
|
|
13
14
|
static readonly __entity = target;
|
|
14
15
|
}
|
|
@@ -34,7 +35,7 @@ export function collectedAs(collector: object, value: unknown): unknown {
|
|
|
34
35
|
|
|
35
36
|
let reader = readers.get(target);
|
|
36
37
|
if (!reader) {
|
|
37
|
-
reader = json(target as
|
|
38
|
+
reader = json(target as Entity);
|
|
38
39
|
readers.set(target, reader);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OperationContract } from '../wire/OperationContract.js';
|
|
2
|
+
|
|
3
|
+
/** The contracts a class carries on itself — `Crud(Post)` states its five as `__ops`. */
|
|
4
|
+
export function opsOf(ctor: unknown): Record<string, OperationContract> {
|
|
5
|
+
return (ctor as { __ops?: Record<string, OperationContract> }).__ops ?? {};
|
|
6
|
+
}
|
package/src/prefab/prefab.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Entity } from '@fougere/schema';
|
|
2
2
|
import type { PresenterViews } from './presenter.js';
|
|
3
3
|
|
|
4
4
|
/** What a prefab class declares about ITSELF, at runtime. */
|
|
5
5
|
interface Prefab {
|
|
6
6
|
/** The shape this class was built on. `Mirror` calls it a shape; it is the same slot. */
|
|
7
|
-
readonly __entity?:
|
|
7
|
+
readonly __entity?: Entity;
|
|
8
8
|
/** The view each computed field emits — `Presenter(Order, { items: [OrderItemView] })`. */
|
|
9
9
|
readonly __views?: PresenterViews;
|
|
10
10
|
/** The view the whole handler speaks — `Crud(Post, PostPublic)`. Same slot, one level up. */
|
|
11
|
-
readonly __output?:
|
|
11
|
+
readonly __output?: Entity;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** The shape a prefab was built on, or `undefined` when the class is not one. */
|
|
15
|
-
export function targetOf(ctor: unknown):
|
|
15
|
+
export function targetOf(ctor: unknown): Entity | undefined {
|
|
16
16
|
return (ctor as Prefab | undefined)?.__entity;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -22,6 +22,6 @@ export function viewsOf(ctor: unknown): PresenterViews | undefined {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** The handler-wide view a prefab declares, same rule. */
|
|
25
|
-
export function outputOf(ctor: unknown):
|
|
25
|
+
export function outputOf(ctor: unknown): Entity | undefined {
|
|
26
26
|
return (ctor as Prefab | undefined)?.__output;
|
|
27
27
|
}
|
package/src/prefab/presenter.ts
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* Documented: [presenters](https://fougere.dev/docs/business/presenters).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { upperFirst, type
|
|
7
|
+
import { upperFirst, type Entity } from '@fougere/schema';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The view a computed field emits — `OrderItemView` for one, `[OrderItemView]` for many.
|
|
11
11
|
* A view is a schema, so it derives: `Order.pick('id', 'status')`, never a hand-written type.
|
|
12
12
|
*/
|
|
13
|
-
export type PresenterViews = Record<string,
|
|
13
|
+
export type PresenterViews = Record<string, Entity | [Entity]>;
|
|
14
14
|
|
|
15
15
|
/** `Presenter(Order, { items: [OrderItemView] })` — the views its computed fields emit. */
|
|
16
|
-
export function Presenter<E extends
|
|
16
|
+
export function Presenter<E extends Entity>(entity: E, views?: PresenterViews) {
|
|
17
17
|
class PresenterBase {
|
|
18
18
|
static readonly __entity = entity;
|
|
19
19
|
static readonly __views = views;
|