@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.
Files changed (181) hide show
  1. package/dist/Declared.d.ts +2 -5
  2. package/dist/Declared.d.ts.map +1 -1
  3. package/dist/DeclaredHandler.d.ts +3 -8
  4. package/dist/DeclaredHandler.d.ts.map +1 -1
  5. package/dist/DeclaredSubject.d.ts +2 -5
  6. package/dist/DeclaredSubject.d.ts.map +1 -1
  7. package/dist/EffectiveOperation.d.ts +7 -6
  8. package/dist/EffectiveOperation.d.ts.map +1 -1
  9. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  10. package/dist/EffectiveOperationModel.js +9 -6
  11. package/dist/EffectiveOperationModel.js.map +1 -1
  12. package/dist/FrondConfig.d.ts +0 -2
  13. package/dist/FrondConfig.d.ts.map +1 -1
  14. package/dist/FrondConfig.js.map +1 -1
  15. package/dist/FrondDeclaration.d.ts +3 -8
  16. package/dist/FrondDeclaration.d.ts.map +1 -1
  17. package/dist/FrondDeclaration.js +2 -1
  18. package/dist/FrondDeclaration.js.map +1 -1
  19. package/dist/OperationOverride.d.ts +5 -3
  20. package/dist/OperationOverride.d.ts.map +1 -1
  21. package/dist/SourceView.d.ts +10 -8
  22. package/dist/SourceView.d.ts.map +1 -1
  23. package/dist/SourceView.js +10 -1
  24. package/dist/SourceView.js.map +1 -1
  25. package/dist/boot/bootstrap.d.ts.map +1 -1
  26. package/dist/boot/bootstrap.js +6 -5
  27. package/dist/boot/bootstrap.js.map +1 -1
  28. package/dist/boot/install.d.ts.map +1 -1
  29. package/dist/boot/install.js +8 -4
  30. package/dist/boot/install.js.map +1 -1
  31. package/dist/boot/unregistered.d.ts +11 -0
  32. package/dist/boot/unregistered.d.ts.map +1 -0
  33. package/dist/boot/unregistered.js +35 -0
  34. package/dist/boot/unregistered.js.map +1 -0
  35. package/dist/contract.d.ts +3 -0
  36. package/dist/contract.d.ts.map +1 -1
  37. package/dist/contract.js +3 -0
  38. package/dist/contract.js.map +1 -1
  39. package/dist/descriptor/CollectorEntry.d.ts +2 -7
  40. package/dist/descriptor/CollectorEntry.d.ts.map +1 -1
  41. package/dist/descriptor/FrondDescriptor.d.ts +5 -23
  42. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  43. package/dist/descriptor/FrondDescriptor.js.map +1 -1
  44. package/dist/descriptor/HandlerEntry.d.ts +2 -7
  45. package/dist/descriptor/HandlerEntry.d.ts.map +1 -1
  46. package/dist/descriptor/MiddlewareEntry.d.ts +2 -7
  47. package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
  48. package/dist/descriptor/PresenterEntry.d.ts +2 -7
  49. package/dist/descriptor/PresenterEntry.d.ts.map +1 -1
  50. package/dist/descriptor/ProviderEntry.d.ts +2 -7
  51. package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
  52. package/dist/descriptor/Subject.d.ts +10 -0
  53. package/dist/descriptor/Subject.d.ts.map +1 -0
  54. package/dist/descriptor/Subject.js +2 -0
  55. package/dist/descriptor/Subject.js.map +1 -0
  56. package/dist/descriptor/index.d.ts +1 -0
  57. package/dist/descriptor/index.d.ts.map +1 -1
  58. package/dist/descriptor/index.js +1 -0
  59. package/dist/descriptor/index.js.map +1 -1
  60. package/dist/index.d.ts +3 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/prefab/AggregateConstructor.d.ts +2 -2
  65. package/dist/prefab/AggregateConstructor.d.ts.map +1 -1
  66. package/dist/prefab/AggregateOf.d.ts +2 -2
  67. package/dist/prefab/AggregateOf.d.ts.map +1 -1
  68. package/dist/prefab/AggregateShape.d.ts +2 -2
  69. package/dist/prefab/AggregateShape.d.ts.map +1 -1
  70. package/dist/prefab/CrudConstructor.d.ts +2 -2
  71. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  72. package/dist/prefab/CrudConstructor.js +12 -10
  73. package/dist/prefab/CrudConstructor.js.map +1 -1
  74. package/dist/prefab/CrudOps.d.ts +2 -2
  75. package/dist/prefab/CrudOps.d.ts.map +1 -1
  76. package/dist/prefab/CrudViews.d.ts +2 -2
  77. package/dist/prefab/CrudViews.d.ts.map +1 -1
  78. package/dist/prefab/MirrorConstructor.d.ts +2 -2
  79. package/dist/prefab/MirrorConstructor.d.ts.map +1 -1
  80. package/dist/prefab/MirrorConstructor.js.map +1 -1
  81. package/dist/prefab/RepositoryConstructor.d.ts +4 -4
  82. package/dist/prefab/RepositoryConstructor.d.ts.map +1 -1
  83. package/dist/prefab/RepositoryConstructor.js.map +1 -1
  84. package/dist/prefab/collector.d.ts +2 -1
  85. package/dist/prefab/collector.d.ts.map +1 -1
  86. package/dist/prefab/collector.js.map +1 -1
  87. package/dist/prefab/opsOf.d.ts +4 -0
  88. package/dist/prefab/opsOf.d.ts.map +1 -0
  89. package/dist/prefab/opsOf.js +5 -0
  90. package/dist/prefab/opsOf.js.map +1 -0
  91. package/dist/prefab/prefab.d.ts +3 -3
  92. package/dist/prefab/prefab.d.ts.map +1 -1
  93. package/dist/prefab/presenter.d.ts +3 -3
  94. package/dist/prefab/presenter.d.ts.map +1 -1
  95. package/dist/prefab/presenter.js.map +1 -1
  96. package/dist/wire/Addresses.d.ts +4 -4
  97. package/dist/wire/Addresses.d.ts.map +1 -1
  98. package/dist/wire/Cardinality.d.ts +4 -0
  99. package/dist/wire/Cardinality.d.ts.map +1 -0
  100. package/dist/wire/Cardinality.js +3 -0
  101. package/dist/wire/Cardinality.js.map +1 -0
  102. package/dist/wire/OperationContract.d.ts +2 -1
  103. package/dist/wire/OperationContract.d.ts.map +1 -1
  104. package/dist/wire/OperationContract.js.map +1 -1
  105. package/dist/wire/OperationKind.d.ts +3 -1
  106. package/dist/wire/OperationKind.d.ts.map +1 -1
  107. package/dist/wire/OperationKind.js +2 -1
  108. package/dist/wire/OperationKind.js.map +1 -1
  109. package/dist/wire/Placement.d.ts +4 -0
  110. package/dist/wire/Placement.d.ts.map +1 -0
  111. package/dist/wire/Placement.js +3 -0
  112. package/dist/wire/Placement.js.map +1 -0
  113. package/dist/wire/RouteKind.d.ts +2 -1
  114. package/dist/wire/RouteKind.d.ts.map +1 -1
  115. package/dist/wire/StateShape.d.ts +1 -2
  116. package/dist/wire/StateShape.d.ts.map +1 -1
  117. package/dist/wire/StateShape.js +11 -11
  118. package/dist/wire/StateShape.js.map +1 -1
  119. package/dist/wire/TypeRef.d.ts +5 -0
  120. package/dist/wire/TypeRef.d.ts.map +1 -1
  121. package/dist/wire/binding.d.ts +4 -1
  122. package/dist/wire/binding.d.ts.map +1 -1
  123. package/dist/wire/binding.js +4 -2
  124. package/dist/wire/binding.js.map +1 -1
  125. package/dist/wire/card/CardOp.d.ts +4 -10
  126. package/dist/wire/card/CardOp.d.ts.map +1 -1
  127. package/dist/wire/refusals.d.ts +2 -1
  128. package/dist/wire/refusals.d.ts.map +1 -1
  129. package/dist/wire/refusals.js.map +1 -1
  130. package/dist/wire/topology/DeclaredFrond.d.ts +2 -1
  131. package/dist/wire/topology/DeclaredFrond.d.ts.map +1 -1
  132. package/dist/wire/topology/FrondPlacement.d.ts +2 -1
  133. package/dist/wire/topology/FrondPlacement.d.ts.map +1 -1
  134. package/package.json +3 -3
  135. package/src/Declared.ts +3 -7
  136. package/src/DeclaredHandler.ts +3 -8
  137. package/src/DeclaredSubject.ts +3 -5
  138. package/src/EffectiveOperation.ts +7 -6
  139. package/src/EffectiveOperationModel.ts +11 -7
  140. package/src/FrondConfig.ts +0 -2
  141. package/src/FrondDeclaration.ts +5 -8
  142. package/src/OperationOverride.ts +5 -3
  143. package/src/SourceView.ts +21 -3
  144. package/src/boot/bootstrap.ts +8 -5
  145. package/src/boot/install.ts +7 -4
  146. package/src/boot/unregistered.ts +40 -0
  147. package/src/contract.ts +3 -0
  148. package/src/descriptor/CollectorEntry.ts +2 -7
  149. package/src/descriptor/FrondDescriptor.ts +5 -19
  150. package/src/descriptor/HandlerEntry.ts +2 -7
  151. package/src/descriptor/MiddlewareEntry.ts +2 -7
  152. package/src/descriptor/PresenterEntry.ts +2 -7
  153. package/src/descriptor/ProviderEntry.ts +2 -7
  154. package/src/descriptor/Subject.ts +10 -0
  155. package/src/descriptor/index.ts +1 -0
  156. package/src/index.ts +3 -1
  157. package/src/prefab/AggregateConstructor.ts +2 -2
  158. package/src/prefab/AggregateOf.ts +2 -2
  159. package/src/prefab/AggregateShape.ts +2 -2
  160. package/src/prefab/CrudConstructor.ts +17 -13
  161. package/src/prefab/CrudOps.ts +3 -3
  162. package/src/prefab/CrudViews.ts +2 -2
  163. package/src/prefab/MirrorConstructor.ts +2 -2
  164. package/src/prefab/RepositoryConstructor.ts +9 -9
  165. package/src/prefab/collector.ts +4 -3
  166. package/src/prefab/opsOf.ts +6 -0
  167. package/src/prefab/prefab.ts +5 -5
  168. package/src/prefab/presenter.ts +3 -3
  169. package/src/wire/Addresses.ts +4 -4
  170. package/src/wire/Cardinality.ts +4 -0
  171. package/src/wire/OperationContract.ts +2 -1
  172. package/src/wire/OperationKind.ts +4 -1
  173. package/src/wire/Placement.ts +4 -0
  174. package/src/wire/RouteKind.ts +3 -1
  175. package/src/wire/StateShape.ts +9 -11
  176. package/src/wire/TypeRef.ts +2 -0
  177. package/src/wire/binding.ts +7 -2
  178. package/src/wire/card/CardOp.ts +4 -10
  179. package/src/wire/refusals.ts +2 -1
  180. package/src/wire/topology/DeclaredFrond.ts +3 -1
  181. package/src/wire/topology/FrondPlacement.ts +3 -1
@@ -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
- ): void {
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
- container.setFallback((name) => (isFacadeKey(name) && servedElsewhere(addressOf(name))
585
- ? facadeOperations(dispatcher, addressOf(name))
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
- if (remoteRouter) answerRemotes(assembly, remoteRouter, fronds);
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;
@@ -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
- kind?: 'query' | 'command';
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
+ }
@@ -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 { SourceView } from './SourceView.js';
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 EntityConstructor } from '@fougere/schema';
2
+ import { type Entity } from '@fougere/schema';
3
3
 
4
- export type AggregateConstructor<E extends readonly EntityConstructor[]> =
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 EntityConstructor } from '@fougere/schema';
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 EntityConstructor[]> =
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 { EntityConstructor } from '@fougere/schema';
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 EntityConstructor[]> {
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 EntityConstructor, type SchemaView } from '@fougere/schema';
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 segment, or a query fallback. */
33
- const byId = { name: 'id', source: { kind: 'param' as const, name: 'id' }, optional: false };
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 = { name: 'id', type: { raw: 'string', name: 'string' } };
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: [byId],
65
- signature: { name: 'findById', returnType: returns(`${name} | undefined`, name, { nullable: true }), params: [idParam] },
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: [byId, fromBody],
75
- signature: { name: 'update', returnType: returns(name, name), params: [idParam, input] },
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: [byId],
79
- signature: { name: 'delete', returnType: returns('boolean', 'boolean'), params: [idParam] },
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 EntityConstructor, V extends CrudViews | EntityConstructor = {}>(
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 EntityConstructor) : undefined;
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 as unknown as SchemaView & { partial?: () => SchemaView });
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.
@@ -1,5 +1,5 @@
1
1
  import type { CrudOpName } from './CrudOpName.js';
2
- import type { EntityConstructor } from '@fougere/schema';
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 [EntityConstructor] ? InstanceType<V & EntityConstructor>
12
- : K extends keyof V ? (V[K] extends EntityConstructor ? InstanceType<V[K]> : T)
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. */
@@ -1,5 +1,5 @@
1
- import { type EntityConstructor } from '@fougere/schema';
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, EntityConstructor>>;
5
+ export type CrudViews = Partial<Record<CrudOpName, Entity>>;
@@ -1,4 +1,4 @@
1
- import type { EntityConstructor } from '@fougere/schema';
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 EntityConstructor>(shape: E): MirrorConstructor<InstanceType<E>> {
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 EntityConstructor } from '@fougere/schema';
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 EntityConstructor[]> {
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 EntityConstructor>(entity: E): RepositoryConstructor<InstanceType<E>>;
21
+ export function Repository<E extends Entity>(entity: E): RepositoryConstructor<InstanceType<E>>;
22
22
 
23
- export function Repository<E extends readonly [EntityConstructor, EntityConstructor, ...EntityConstructor[]]>(
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: EntityConstructor[]): unknown {
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 EntityConstructor>(entity: E): RepositoryConstructor<InstanceType<E>> {
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 EntityConstructor[]>(entities: E): AggregateConstructor<E> {
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 EntityConstructor[] {
81
- return (ctor as { __owns?: readonly EntityConstructor[] } | undefined)?.__owns ?? [];
80
+ export function ownedBy(ctor: unknown): readonly Entity[] {
81
+ return (ctor as { __owns?: readonly Entity[] } | undefined)?.__owns ?? [];
82
82
  }
@@ -4,11 +4,12 @@
4
4
  * Documented: [collectors](https://fougere.dev/docs/business/collectors).
5
5
  */
6
6
 
7
- import { FieldValueValidator, dotted, json, upperFirst, type Field, type SchemaView } from '@fougere/schema';
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 abstract new (...args: any[]) => any>(target: T) {
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 SchemaView & (new () => unknown));
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
+ }
@@ -1,18 +1,18 @@
1
- import type { EntityConstructor } from '@fougere/schema';
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?: EntityConstructor;
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?: EntityConstructor;
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): EntityConstructor | undefined {
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): EntityConstructor | undefined {
25
+ export function outputOf(ctor: unknown): Entity | undefined {
26
26
  return (ctor as Prefab | undefined)?.__output;
27
27
  }
@@ -4,16 +4,16 @@
4
4
  * Documented: [presenters](https://fougere.dev/docs/business/presenters).
5
5
  */
6
6
 
7
- import { upperFirst, type EntityConstructor } from '@fougere/schema';
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, EntityConstructor | [EntityConstructor]>;
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 EntityConstructor>(entity: E, views?: PresenterViews) {
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;