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