@fougere/core 0.12.1-alpha.0 → 0.12.4-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/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts +1 -4
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +182 -173
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +5 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +11 -13
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +1 -5
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/Source.js +3 -3
- package/dist/Source.js.map +1 -1
- package/dist/SourceView.d.ts +0 -2
- package/dist/SourceView.d.ts.map +1 -1
- package/dist/boot/App.d.ts +2 -4
- package/dist/boot/App.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +5 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +19 -10
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/AuthRuntime.d.ts +5 -12
- package/dist/boot/AuthRuntime.d.ts.map +1 -1
- package/dist/boot/AuthRuntime.js +2 -1
- package/dist/boot/AuthRuntime.js.map +1 -1
- package/dist/boot/CreateAppOptions.d.ts +0 -11
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +3 -6
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/Hosting.d.ts +2 -1
- package/dist/boot/Hosting.d.ts.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +143 -265
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +105 -115
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts +5 -0
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js +8 -0
- package/dist/descriptor/Fronds.js.map +1 -1
- 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/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts +3 -2
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js +11 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Comparison.d.ts +2 -2
- package/dist/storage/Comparison.d.ts.map +1 -1
- package/dist/storage/Comparison.js +3 -1
- package/dist/storage/Comparison.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +1 -1
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/Emit.d.ts +0 -2
- package/dist/wire/Emit.d.ts.map +1 -1
- package/dist/wire/Emit.js +1 -1
- package/dist/wire/Emit.js.map +1 -1
- package/dist/wire/Facade.d.ts +7 -0
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js +13 -0
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts +0 -4
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/OperationsMap.d.ts +0 -3
- package/dist/wire/OperationsMap.d.ts.map +1 -1
- package/dist/wire/OperationsMap.js +0 -5
- package/dist/wire/OperationsMap.js.map +1 -1
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +236 -198
- package/src/FougereConfig.ts +17 -16
- package/src/FrondConfig.ts +2 -0
- package/src/FrondDeclaration.ts +1 -7
- package/src/Source.ts +3 -3
- package/src/SourceView.ts +0 -2
- package/src/boot/App.ts +2 -4
- package/src/boot/AppLifecycle.ts +22 -10
- package/src/boot/AuthRuntime.ts +6 -13
- package/src/boot/CreateAppOptions.ts +0 -11
- package/src/boot/Emissions.ts +7 -7
- package/src/boot/Hosting.ts +2 -1
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +175 -300
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +123 -146
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +42 -10
- package/src/boot/remote.ts +1 -0
- package/src/boot/seed.ts +3 -5
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +1 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +12 -0
- package/src/descriptor/index.ts +1 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +18 -2
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +2 -4
- package/src/loader.ts +1 -0
- package/src/node.ts +1 -1
- package/src/prefab/CrudConstructor.ts +1 -11
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Comparison.ts +7 -4
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/Emit.ts +1 -1
- package/src/wire/Facade.ts +17 -10
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/OperationsMap.ts +0 -10
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
- package/dist/boot/AuthConfig.d.ts +0 -14
- package/dist/boot/AuthConfig.d.ts.map +0 -1
- package/dist/boot/AuthConfig.js +0 -2
- package/dist/boot/AuthConfig.js.map +0 -1
- package/dist/boot/AuthContext.d.ts +0 -12
- package/dist/boot/AuthContext.d.ts.map +0 -1
- package/dist/boot/AuthContext.js +0 -2
- package/dist/boot/AuthContext.js.map +0 -1
- package/dist/storage/ComparisonName.d.ts +0 -3
- package/dist/storage/ComparisonName.d.ts.map +0 -1
- package/dist/storage/ComparisonName.js +0 -2
- package/dist/storage/ComparisonName.js.map +0 -1
- package/src/boot/AuthConfig.ts +0 -14
- package/src/boot/AuthContext.ts +0 -12
- package/src/storage/ComparisonName.ts +0 -3
package/src/boot/card.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
|
|
|
37
37
|
const ops = facadeOps(app, address, surface);
|
|
38
38
|
if (ops.length === 0) return [];
|
|
39
39
|
const entity = byEntity.get(address);
|
|
40
|
+
|
|
40
41
|
return [{
|
|
41
42
|
name: address,
|
|
42
43
|
ops,
|
|
@@ -47,6 +48,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
|
|
|
47
48
|
/** What leaves on its own — the same list on every surface, deliberately. */
|
|
48
49
|
facts: factsAnnouncedBy(frond.handlers).map((name) => {
|
|
49
50
|
const entityClass = declared.get(name);
|
|
51
|
+
|
|
50
52
|
return { name, ...(entityClass ? { schema: Card.fromSchema(entityClass, name).descriptor } : {}) };
|
|
51
53
|
}),
|
|
52
54
|
};
|
package/src/boot/frame.ts
CHANGED
|
@@ -83,6 +83,7 @@ function recordCreate({ base, recorded, entity, key, journal }: Recording): void
|
|
|
83
83
|
const row = await base.create.apply(this, args);
|
|
84
84
|
const id = String(row[key]);
|
|
85
85
|
journal.push({ what: `create ${entity}#${id}`, run: () => base.delete.call(this, id).then(() => undefined) });
|
|
86
|
+
|
|
86
87
|
return row;
|
|
87
88
|
};
|
|
88
89
|
}
|
|
@@ -119,6 +120,7 @@ function recordUpdate({ base, recorded, entity, journal }: Recording): void {
|
|
|
119
120
|
await base.update.call(this, id, pick(before, touched));
|
|
120
121
|
},
|
|
121
122
|
});
|
|
123
|
+
|
|
122
124
|
return row;
|
|
123
125
|
};
|
|
124
126
|
}
|
|
@@ -131,6 +133,7 @@ function recordDelete({ base, recorded, entity, journal }: Recording): void {
|
|
|
131
133
|
if (removed && before) {
|
|
132
134
|
journal.push({ what: `delete ${entity}#${id}`, run: () => base.create.call(this, before).then(() => undefined) });
|
|
133
135
|
}
|
|
136
|
+
|
|
134
137
|
return removed;
|
|
135
138
|
};
|
|
136
139
|
}
|
|
@@ -144,6 +147,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
144
147
|
const undoUpsert = async function (this: unknown, rows: readonly Record<string, unknown>[]): Promise<Undo> {
|
|
145
148
|
const ids = rows.map((row) => String(row[key]));
|
|
146
149
|
const before = await base.findByKeys.call(this, ids);
|
|
150
|
+
|
|
147
151
|
return {
|
|
148
152
|
what: `upsert ${entity} (${ids.length} row(s))`,
|
|
149
153
|
run: async () => {
|
|
@@ -163,6 +167,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
163
167
|
const undo = await undoUpsert.call(this, [input]);
|
|
164
168
|
const row = await upsert.call(this, input, ...rest);
|
|
165
169
|
journal.push(undo);
|
|
170
|
+
|
|
166
171
|
return row;
|
|
167
172
|
};
|
|
168
173
|
}
|
|
@@ -174,6 +179,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
|
|
|
174
179
|
const undo = await undoUpsert.call(this, inputs);
|
|
175
180
|
const written = await upsertAll.call(this, inputs, ...rest);
|
|
176
181
|
journal.push(undo);
|
|
182
|
+
|
|
177
183
|
return written;
|
|
178
184
|
};
|
|
179
185
|
}
|
package/src/boot/install.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Diagnostic } from '../diagnostic.js';
|
|
|
21
21
|
import { HandlerFacade } from '../dispatch/HandlerFacade.js';
|
|
22
22
|
import { targetOf } from '../prefab/prefab.js';
|
|
23
23
|
import { ownersOf, sharedNames, storageInUserCode, crudOnOwned } from './ownership.js';
|
|
24
|
+
import { refuseArityDrift } from './arity.js';
|
|
24
25
|
import { StorageGuard } from '../dispatch/StorageGuard.js';
|
|
25
26
|
import { portBindings, seamChains, wrapping, SEAMS } from './ports.js';
|
|
26
27
|
import { contractsKeyOf, facadeKeyOf } from '../wire/Facade.js';
|
|
@@ -307,7 +308,7 @@ function registerProviders(
|
|
|
307
308
|
// of: the container resolves a dep by NAME, so wrapping is a substituted key and
|
|
308
309
|
// needs nothing of the container itself. The outermost answers under the port.
|
|
309
310
|
let inner = nameOf(chain.at(-1)!);
|
|
310
|
-
for (const wrapper of
|
|
311
|
+
for (const wrapper of chain.slice(0, -1).reverse()) {
|
|
311
312
|
const deps = wrapper.deps.map((dep) => (dep === port ? inner : dep));
|
|
312
313
|
inner = nameOf(wrapper);
|
|
313
314
|
scope.register(inner, wrapper.ctor, { deps });
|
|
@@ -456,88 +457,51 @@ function buildFacadeInto(
|
|
|
456
457
|
));
|
|
457
458
|
}
|
|
458
459
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// register nothing locally — resolve() falls through to the remote façade.
|
|
468
|
-
if (options.remotes && frond.name in options.remotes) {
|
|
469
|
-
log.child(frond.name).info('declared remote — not hosted locally');
|
|
470
|
-
// Its facades answer elsewhere, but what they LISTEN to was read here.
|
|
471
|
-
for (const handler of frond.handlers) {
|
|
472
|
-
const key = facadeKeyOf(handler.address, handler.surface);
|
|
473
|
-
const operations = operationModel.forHandler(handler);
|
|
474
|
-
effectiveByKey.set(key, operations);
|
|
475
|
-
emissions.note(contractsOf(operations), key);
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
return;
|
|
460
|
+
/** Its facades answer elsewhere, but what they LISTEN to is read here, and the bridges route with it. */
|
|
461
|
+
function noteRemoteFrond(frond: FrondDescriptor, { operationModel, effectiveByKey, emissions, contractsOf, log }: Assembly): void {
|
|
462
|
+
log.child(frond.name).info('declared remote — not hosted locally');
|
|
463
|
+
for (const handler of frond.handlers) {
|
|
464
|
+
const key = facadeKeyOf(handler.address, handler.surface);
|
|
465
|
+
const operations = operationModel.forHandler(handler);
|
|
466
|
+
effectiveByKey.set(key, operations);
|
|
467
|
+
emissions.note(contractsOf(operations), key);
|
|
479
468
|
}
|
|
480
|
-
|
|
481
|
-
// nothing else has to know: `ScopeContainer.resolve` already walks up. The boot installs
|
|
482
|
-
// parents first, which is what makes the key below resolvable.
|
|
483
|
-
const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
|
|
484
|
-
const scope = above.createScope();
|
|
485
|
-
const frondLog = log.child(frond.name);
|
|
486
|
-
// The frond's own voice, a child of the APP logger and never of `log` — which is the
|
|
487
|
-
// boot's, so a service's line would have claimed `boot:` long after the boot was over.
|
|
488
|
-
scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
|
|
489
|
-
|
|
490
|
-
// `reads:` is what makes a cross-source reader exist here, and the list IS its
|
|
491
|
-
// environment — a source holding none of these is never opened. Registered under
|
|
492
|
-
// the type's own name, which is the key `depKeyOf` already derives for a plain
|
|
493
|
-
// parameter: `constructor(private reads: Reads)` and nothing else to say.
|
|
494
|
-
// Declaring `reads:` with nothing to build the reader is a boot that ignores a
|
|
495
|
-
// clause: the handler asking for `Reads` then dies at its first call, on a
|
|
496
|
-
// container message that names neither the clause nor what is missing.
|
|
497
|
-
await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
|
|
469
|
+
}
|
|
498
470
|
|
|
499
|
-
|
|
500
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Who owns what, and the rule that makes owning mean something — refused before anything is
|
|
473
|
+
* registered, so a bad line is named here rather than by the container.
|
|
474
|
+
*/
|
|
475
|
+
function ownershipOf(frond: FrondDescriptor, { entityByName, refused }: Assembly): Map<string, string> {
|
|
501
476
|
sharedNames(frond, refused);
|
|
477
|
+
refuseArityDrift(frond, refused);
|
|
502
478
|
const owners = ownersOf(frond.providers, frond.name, refused);
|
|
503
479
|
storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
|
|
504
480
|
crudOnOwned(frond, owners, refused);
|
|
505
481
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// like a middleware, a link cannot be inherited through a container key, since a seam has
|
|
509
|
-
// none: its realization is built rather than resolved.
|
|
510
|
-
const seams = inheritedSeams(frond, assembly.seamsOf, declared);
|
|
511
|
-
assembly.seamsOf.set(frond.name, seams);
|
|
482
|
+
return owners;
|
|
483
|
+
}
|
|
512
484
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
485
|
+
/** A frame is read from the same `deps` every port is: asking for one IS declaring it. */
|
|
486
|
+
function registerFramesOf(frond: FrondDescriptor, assembly: Assembly, scope: Container, frondLog: Logger): void {
|
|
487
|
+
const { entityByName, frondOf, options, hosting, refused } = assembly;
|
|
488
|
+
const wanted = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
|
|
489
|
+
.flatMap((declared) => declared.deps.map((key) => ({ key, filePath: declared.filePath })));
|
|
490
|
+
|
|
491
|
+
registerFrames(scope, wanted, frond.providers, {
|
|
492
|
+
entityByName,
|
|
493
|
+
frondOf,
|
|
494
|
+
hostedHere: (name) => !(options.remotes && name in options.remotes),
|
|
495
|
+
storageFactory: options.storageFactory,
|
|
496
|
+
sourceOf: options.sourceOf,
|
|
497
|
+
transacts: options.transacts,
|
|
498
|
+
transacted: options.transacted,
|
|
499
|
+
hosting,
|
|
500
|
+
log: frondLog,
|
|
501
|
+
}, refused);
|
|
502
|
+
}
|
|
516
503
|
|
|
517
|
-
|
|
518
|
-
// from the same `deps` every other port is read from — asking for it IS declaring it,
|
|
519
|
-
// so nothing is registered for a frame nobody wants.
|
|
520
|
-
registerFrames(
|
|
521
|
-
scope,
|
|
522
|
-
[...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
|
|
523
|
-
.flatMap((d) => d.deps.map((key) => ({ key, filePath: d.filePath }))),
|
|
524
|
-
frond.providers,
|
|
525
|
-
{
|
|
526
|
-
entityByName,
|
|
527
|
-
frondOf,
|
|
528
|
-
hostedHere: (name) => !(options.remotes && name in options.remotes),
|
|
529
|
-
storageFactory: options.storageFactory,
|
|
530
|
-
sourceOf: options.sourceOf,
|
|
531
|
-
transacts: options.transacts,
|
|
532
|
-
transacted: options.transacted,
|
|
533
|
-
hosting: assembly.hosting,
|
|
534
|
-
log: frondLog,
|
|
535
|
-
},
|
|
536
|
-
refused,
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
// Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
|
|
540
|
-
const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
|
|
504
|
+
function registerPresenters(frond: FrondDescriptor, scope: Container, frondLog: Logger): Map<string, PresenterEntry> {
|
|
541
505
|
for (const presenter of frond.presenters) {
|
|
542
506
|
scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
|
|
543
507
|
}
|
|
@@ -545,104 +509,117 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
|
|
|
545
509
|
frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
|
|
546
510
|
}
|
|
547
511
|
|
|
548
|
-
|
|
549
|
-
|
|
512
|
+
return new Map(frond.presenters.map((presenter) => [presenter.entityName, presenter]));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function registerCollectors(frond: FrondDescriptor, scope: Container, frondLog: Logger): Set<string> {
|
|
550
516
|
for (const collector of frond.collectors) {
|
|
551
|
-
|
|
552
|
-
scope.register(key, collector.ctor, { deps: collector.deps });
|
|
517
|
+
scope.register(collectorKeyOf(collector.typeName), collector.ctor, { deps: collector.deps });
|
|
553
518
|
}
|
|
554
519
|
if (frond.collectors.length > 0) {
|
|
555
520
|
frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
|
|
556
521
|
}
|
|
557
522
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// instance the boot built — the same reason `getMiddlewares` is read at call time.
|
|
561
|
-
registerMiddlewares(frond, scope, assembly, frondLog);
|
|
562
|
-
|
|
563
|
-
// Build handler facades → registered in ROOT container (public contract)
|
|
564
|
-
const defaultHandlers = frond.handlers.filter((h) => !h.surface);
|
|
565
|
-
const surfaceHandlers = frond.handlers.filter((h) => h.surface);
|
|
566
|
-
const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
|
|
523
|
+
return new Set(frond.collectors.map((collector) => collector.typeName));
|
|
524
|
+
}
|
|
567
525
|
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
facadeKey: string,
|
|
574
|
-
) => buildFacadeInto(building, entity, handler, targetScope, facadeKey);
|
|
575
|
-
|
|
576
|
-
// A presenter is about an entity — computed fields sit on a shape — so this walks
|
|
577
|
-
// entities. Exposing the instance lazily; the bridge resolves it on first access.
|
|
578
|
-
exposePresenters(frond, presenterMap, container, scope);
|
|
526
|
+
function sayFacade(building: Building, facadeKey: string, entity: EntityEntry | undefined): void {
|
|
527
|
+
const operations = Object.keys(building.assembly.container.resolve(facadeKey) as object).join(', ');
|
|
528
|
+
building.frondLog.debug(`${facadeKey} [${operations}]`
|
|
529
|
+
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
530
|
+
}
|
|
579
531
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
//
|
|
588
|
-
// By NAME, not by identity: the scanner loads an entity through its own loader and
|
|
589
|
-
// the handler imports it through the runtime's, so the same class arrives as two
|
|
590
|
-
// objects. `===` compares module instances, which is not the question being asked.
|
|
532
|
+
/**
|
|
533
|
+
* A facade is about a handler, so this walks HANDLERS — pointing at no entity is legal. The
|
|
534
|
+
* subject is `Crud(Item)`'s when it names one, the handler's own name otherwise, compared by
|
|
535
|
+
* NAME: the scanner and the runtime load one class as two objects.
|
|
536
|
+
*/
|
|
537
|
+
function buildDefaultFacades(building: Building, handlers: readonly HandlerEntry[]): void {
|
|
538
|
+
for (const handler of handlers) {
|
|
591
539
|
const crudTarget = targetOf(handler.ctor);
|
|
592
540
|
const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
|
|
593
|
-
const entity = frond.entities.find((e) => e.name === subject);
|
|
541
|
+
const entity = building.frond.entities.find((e) => e.name === subject);
|
|
594
542
|
const facadeKey = facadeKeyOf(handler.address);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
543
|
+
buildFacadeInto(building, entity, handler, building.scope, facadeKey);
|
|
544
|
+
sayFacade(building, facadeKey, entity);
|
|
598
545
|
}
|
|
546
|
+
}
|
|
599
547
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
548
|
+
/** A shape that declares no operation answers nothing — said once per entity rather than deduced from a silence. */
|
|
549
|
+
function sayUnhandledEntities(building: Building, handlers: readonly HandlerEntry[]): void {
|
|
550
|
+
const handled = new Set(handlers.map((handler) => handler.address));
|
|
551
|
+
for (const entity of building.frond.entities) {
|
|
552
|
+
if (!handled.has(entity.name)) building.frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
|
|
606
553
|
}
|
|
554
|
+
}
|
|
607
555
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// Register scoped storage if output override differs from entity — under the REPOSITORY
|
|
619
|
-
// key, which is what a Crud handler asks for, and under the port's own for a holder
|
|
620
|
-
// that legitimately names it. Registering only the latter left a named surface with
|
|
621
|
-
// no facade at all once the façade stopped spelling the storage.
|
|
622
|
-
if (entity) registerSurfaceStorage(entity, handler, surfaceScope, assembly, frondLog);
|
|
556
|
+
/**
|
|
557
|
+
* One sub-scope per surface handler, holding the storage its view scopes — under the REPOSITORY
|
|
558
|
+
* key a Crud handler asks for. Pointing at no entity is legal here too: one rule, both surfaces.
|
|
559
|
+
*/
|
|
560
|
+
function buildSurfaceFacades(building: Building, handlers: readonly HandlerEntry[]): void {
|
|
561
|
+
for (const handler of handlers) {
|
|
562
|
+
const entity = building.frond.entities.find((e) => e.name === handler.address);
|
|
563
|
+
const surfaceScope = building.scope.createScope();
|
|
564
|
+
if (entity) registerSurfaceStorage(entity, handler, surfaceScope, building.assembly, building.frondLog);
|
|
623
565
|
|
|
624
566
|
const facadeKey = facadeKeyOf(handler.address, handler.surface);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
567
|
+
buildFacadeInto(building, entity, handler, surfaceScope, facadeKey);
|
|
568
|
+
sayFacade(building, facadeKey, entity);
|
|
628
569
|
}
|
|
570
|
+
}
|
|
629
571
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// absent HERE, in one line, instead of being discovered missing later.
|
|
634
|
-
const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
|
|
572
|
+
/** A named surface is closed, so what it serves — and what it leaves out — is said at boot, in one line. */
|
|
573
|
+
function sayWhatSurfacesServe(frond: FrondDescriptor, handlers: readonly HandlerEntry[], frondLog: Logger): void {
|
|
574
|
+
const surfaceNames = [...new Set(handlers.map((h) => h.surface as string))].sort();
|
|
635
575
|
for (const surfaceName of surfaceNames) {
|
|
636
|
-
const served =
|
|
637
|
-
.filter((h) => h.surface === surfaceName)
|
|
638
|
-
.map((h) => h.address)
|
|
639
|
-
.sort();
|
|
576
|
+
const served = handlers.filter((h) => h.surface === surfaceName).map((h) => h.address).sort();
|
|
640
577
|
const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
|
|
641
578
|
frondLog.info(
|
|
642
579
|
`surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
|
|
643
580
|
(absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
|
|
644
581
|
);
|
|
645
582
|
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
|
|
586
|
+
const { container, entityByName, options, log } = assembly;
|
|
587
|
+
|
|
588
|
+
if (options.remotes && frond.name in options.remotes) {
|
|
589
|
+
noteRemoteFrond(frond, assembly);
|
|
590
|
+
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// A child hangs off its parent, so what the parent registered answers here too — parents are installed first.
|
|
595
|
+
const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
|
|
596
|
+
const scope = above.createScope();
|
|
597
|
+
const frondLog = log.child(frond.name);
|
|
598
|
+
// A child of the APP logger, never of the boot's, whose `boot:` would outlive the boot.
|
|
599
|
+
scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
|
|
600
|
+
|
|
601
|
+
await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
|
|
602
|
+
const owners = ownershipOf(frond, assembly);
|
|
603
|
+
|
|
604
|
+
const declared = registerProviders(frond, scope, options.ports, assembly.boundPorts, assembly.refused, frondLog);
|
|
605
|
+
// An ancestor's links stand OUTSIDE this frond's own; a seam has no container key to inherit through.
|
|
606
|
+
const seams = inheritedSeams(frond, assembly.seamsOf, declared);
|
|
607
|
+
assembly.seamsOf.set(frond.name, seams);
|
|
608
|
+
registerStorages(frond, assembly, scope, seams, owners, frondLog);
|
|
609
|
+
registerFramesOf(frond, assembly, scope, frondLog);
|
|
610
|
+
|
|
611
|
+
const presenterMap = registerPresenters(frond, scope, frondLog);
|
|
612
|
+
const collectorTypeNames = registerCollectors(frond, scope, frondLog);
|
|
613
|
+
registerMiddlewares(frond, scope, assembly, frondLog);
|
|
614
|
+
exposePresenters(frond, presenterMap, container, scope);
|
|
615
|
+
|
|
616
|
+
const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
|
|
617
|
+
const defaultHandlers = frond.handlers.filter((h) => !h.surface);
|
|
618
|
+
const surfaceHandlers = frond.handlers.filter((h) => h.surface);
|
|
619
|
+
buildDefaultFacades(building, defaultHandlers);
|
|
620
|
+
sayUnhandledEntities(building, defaultHandlers);
|
|
621
|
+
buildSurfaceFacades(building, surfaceHandlers);
|
|
622
|
+
sayWhatSurfacesServe(frond, surfaceHandlers, frondLog);
|
|
646
623
|
|
|
647
624
|
container.registerValue(`frond:${frond.name}`, scope);
|
|
648
625
|
frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
|
package/src/boot/ownership.ts
CHANGED
|
@@ -80,6 +80,7 @@ function builtOn(ctor: unknown): string[] {
|
|
|
80
80
|
const owned = ownedBy(ctor);
|
|
81
81
|
if (owned.length > 0) return owned.map((e) => lowerFirst((e as { name: string }).name));
|
|
82
82
|
const target = targetOf(ctor) as { name?: string } | undefined;
|
|
83
|
+
|
|
83
84
|
return target?.name ? [lowerFirst(target.name)] : [];
|
|
84
85
|
}
|
|
85
86
|
|
package/src/boot/relations.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { Dependent } from '../dispatch/Dependent.js';
|
|
|
4
4
|
import type { RelationCheck } from '../dispatch/RelationCheck.js';
|
|
5
5
|
import type { Hosting } from './Hosting.js';
|
|
6
6
|
import type { Diagnostic } from '../diagnostic.js';
|
|
7
|
-
import type { Releasing
|
|
7
|
+
import type { Releasing } from '../dispatch/Release.js';
|
|
8
|
+
import type { EntityEntry } from '../descriptor/EntityEntry.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Does a foreign key hold this reference? Both sides in ONE source that keeps relations — two
|
|
@@ -34,7 +35,7 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
|
|
|
34
35
|
for (const [entity, schema] of hosting.entities()) {
|
|
35
36
|
for (const [field, declared] of Object.entries(schema.getFields())) {
|
|
36
37
|
const role = Role.of(declared);
|
|
37
|
-
if (role.isReference && role.target?.name && lowerFirst(role.target.name) === target) {
|
|
38
|
+
if (role.isReference() && role.target?.name && lowerFirst(role.target.name) === target) {
|
|
38
39
|
found.push({ entity, field, role });
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -43,11 +44,6 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
|
|
|
43
44
|
return found;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/** What a target's storage has to answer for a key set to be judged in one read. */
|
|
47
|
-
interface ByKeys {
|
|
48
|
-
findByKeys(keys: readonly string[]): Promise<Map<string, unknown>>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
/**
|
|
52
48
|
* The references a foreign key cannot hold, each as a read the guard makes before the write.
|
|
53
49
|
*
|
|
@@ -58,7 +54,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
|
|
|
58
54
|
|
|
59
55
|
for (const [field, declared] of Object.entries(entity.getFields())) {
|
|
60
56
|
const role = Role.of(declared);
|
|
61
|
-
if (!role.isReference || !role.target?.name) continue;
|
|
57
|
+
if (!role.isReference() || !role.target?.name) continue;
|
|
62
58
|
|
|
63
59
|
const target = lowerFirst(role.target.name);
|
|
64
60
|
if (keyed(name, target, hosting)) continue;
|
|
@@ -67,7 +63,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
|
|
|
67
63
|
field,
|
|
68
64
|
target,
|
|
69
65
|
missing: async (keys) => {
|
|
70
|
-
const storage = hosting.storageOf(target)
|
|
66
|
+
const storage = hosting.storageOf(target);
|
|
71
67
|
// Nothing here holds those rows — the process that does answers the same reading,
|
|
72
68
|
// so the question crosses rather than being given up on.
|
|
73
69
|
if (!storage) return hosting.peerOf(target)?.missing(target, keys) ?? [];
|
|
@@ -122,6 +118,42 @@ export function refuseUnwritableNull(entity: SchemaView, name: string, filePath:
|
|
|
122
118
|
return refused;
|
|
123
119
|
}
|
|
124
120
|
|
|
121
|
+
/**
|
|
122
|
+
* A `many()` whose target declares no `ref()` back. The key lives on the far row, so a
|
|
123
|
+
* relation neither side states can never be filled: SQL emits no column, GraphQL drops the
|
|
124
|
+
* field when it finds no reverse key, and nothing says why.
|
|
125
|
+
*
|
|
126
|
+
* Asked once every frond is installed, since the target may belong to another one — and left
|
|
127
|
+
* alone when it answers elsewhere, where its fields are declared and cannot be read here.
|
|
128
|
+
*/
|
|
129
|
+
export function unpaired(entities: readonly EntityEntry[], hosting: Hosting): Diagnostic[] {
|
|
130
|
+
const refused: Diagnostic[] = [];
|
|
131
|
+
|
|
132
|
+
for (const entry of entities) {
|
|
133
|
+
for (const [field, declared] of Object.entries(entry.entityClass.getFields())) {
|
|
134
|
+
const role = Role.of(declared);
|
|
135
|
+
const target = role.isCollection() ? role.target?.name : undefined;
|
|
136
|
+
if (!target || !hosting.hostedHere(lowerFirst(target))) continue;
|
|
137
|
+
|
|
138
|
+
const held = referencesTo(lowerFirst(entry.name), hosting)
|
|
139
|
+
.some((one) => one.entity === lowerFirst(target));
|
|
140
|
+
if (held) continue;
|
|
141
|
+
|
|
142
|
+
refused.push({
|
|
143
|
+
severity: 'blocking',
|
|
144
|
+
code: 'many-without-reference',
|
|
145
|
+
filePath: entry.filePath,
|
|
146
|
+
subject: `${entry.name}.${field}`,
|
|
147
|
+
message: `${entry.entityClass.name}.${field} states many(${target}), and no field of `
|
|
148
|
+
+ `${target} references ${entry.entityClass.name}: the relation is declared on neither `
|
|
149
|
+
+ `side, so nothing can fill it. Declare the ref() on ${target}.`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return refused;
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
/** The checks nothing in this process can answer — asked once every frond has registered. */
|
|
126
158
|
export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting): string[] {
|
|
127
159
|
return checks
|
|
@@ -139,7 +171,7 @@ export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting):
|
|
|
139
171
|
export function releasing(hosting: Hosting): Releasing {
|
|
140
172
|
return {
|
|
141
173
|
dependentsOf: (entity) => dependentsOf(entity, hosting),
|
|
142
|
-
rowsOf: (entity) => hosting.storageOf(entity)
|
|
174
|
+
rowsOf: (entity) => hosting.storageOf(entity),
|
|
143
175
|
schemaOf: (entity) => hosting.entities().get(entity),
|
|
144
176
|
peers: () => hosting.peers(),
|
|
145
177
|
journal: (entity) => hosting.journal(entity),
|
package/src/boot/remote.ts
CHANGED
|
@@ -54,6 +54,7 @@ export function createRemoteRouter(
|
|
|
54
54
|
transports.set(url, transport);
|
|
55
55
|
try {
|
|
56
56
|
const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, Invocation.empty);
|
|
57
|
+
|
|
57
58
|
// Judged below and not here: this catch means "unreachable, retry", and a
|
|
58
59
|
// refusal thrown inside it would be swallowed into another silent retry.
|
|
59
60
|
return { label, url, transport, answer };
|
package/src/boot/seed.ts
CHANGED
|
@@ -47,7 +47,7 @@ function whatEachWaitsFor(
|
|
|
47
47
|
for (const entity of frond.entities) {
|
|
48
48
|
const targets = new Set<string>();
|
|
49
49
|
for (const field of Object.values(entity.entityClass.getFields())) {
|
|
50
|
-
if (!Role.of(field).isReference) continue;
|
|
50
|
+
if (!Role.of(field).isReference()) continue;
|
|
51
51
|
const target = (Role.of(field).target as { name?: string }).name?.toLowerCase();
|
|
52
52
|
if (target && target !== entity.name.toLowerCase()) targets.add(target);
|
|
53
53
|
}
|
|
@@ -67,7 +67,7 @@ function whatEachWaitsFor(
|
|
|
67
67
|
/** Where a seed writes, and what it may skip — resolved per entity. */
|
|
68
68
|
interface SeedFacade {
|
|
69
69
|
list(): Promise<unknown[]>;
|
|
70
|
-
write(item: unknown): Promise<unknown>;
|
|
70
|
+
write(item: Record<string, unknown>): Promise<unknown>;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -126,9 +126,7 @@ function facadeFor(app: App, entityName: string): SeedFacade | undefined {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const storage = app.storageFor(entityName)
|
|
130
|
-
| { list: () => Promise<unknown[]>; create: (input: unknown) => Promise<unknown> }
|
|
131
|
-
| undefined;
|
|
129
|
+
const storage = app.storageFor(entityName);
|
|
132
130
|
if (!storage) return undefined;
|
|
133
131
|
|
|
134
132
|
return { list: () => storage.list(), write: (item) => storage.create(item) };
|
|
@@ -41,3 +41,28 @@ export function statementDrift(frond: FrondDescriptor, handler: HandlerEntry): D
|
|
|
41
41
|
|
|
42
42
|
return found;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A contract with no method under its name. The scan cannot produce one; a declaration can, and
|
|
47
|
+
* its first call used to fail as `instance[method] is not a function` — a server error for what
|
|
48
|
+
* is a typo in the declaration.
|
|
49
|
+
*/
|
|
50
|
+
export function unansweredOperations(
|
|
51
|
+
frond: FrondDescriptor,
|
|
52
|
+
handler: HandlerEntry,
|
|
53
|
+
names: Iterable<string>,
|
|
54
|
+
): Diagnostic[] {
|
|
55
|
+
const prototype = handler.ctor.prototype as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
return [...names]
|
|
58
|
+
.filter((name) => typeof prototype[name] !== 'function')
|
|
59
|
+
.map((name) => ({
|
|
60
|
+
severity: 'blocking',
|
|
61
|
+
code: 'operation-without-method',
|
|
62
|
+
filePath: handler.filePath,
|
|
63
|
+
frond: frond.name,
|
|
64
|
+
subject: `${handler.ctor.name}.${name}`,
|
|
65
|
+
message: `${handler.ctor.name}.${name} is declared as an operation, and ${handler.ctor.name} `
|
|
66
|
+
+ 'has no method of that name — its first call would fail as a server error.',
|
|
67
|
+
}));
|
|
68
|
+
}
|
package/src/boot/together.ts
CHANGED
|
@@ -154,13 +154,16 @@ async function inScope<R>(
|
|
|
154
154
|
const storages = members.entities.map((member) => {
|
|
155
155
|
const storage = wrap(factory(member.schema, member.name), member.name, member.schema);
|
|
156
156
|
scope.registerValue(storageKeyOf(member.name), storage);
|
|
157
|
+
|
|
157
158
|
return storage;
|
|
158
159
|
});
|
|
159
160
|
// Providers after every storage is in place: one may depend on another member's.
|
|
160
161
|
const built = members.providers.map((provider) => {
|
|
161
162
|
scope.register(provider.ctor.name, provider.ctor, { deps: provider.deps });
|
|
163
|
+
|
|
162
164
|
return scope.resolve(provider.ctor.name);
|
|
163
165
|
});
|
|
166
|
+
|
|
164
167
|
return await fn(storages, built);
|
|
165
168
|
} finally {
|
|
166
169
|
await scope.dispose();
|
package/src/builtin/Carry.ts
CHANGED
package/src/builtin/Logger.ts
CHANGED
|
@@ -12,6 +12,7 @@ let threshold: number = LEVELS[envLevel() ?? 'info'];
|
|
|
12
12
|
/** The level the PROCESS was started with. */
|
|
13
13
|
export function envLevel(): LogLevel | undefined {
|
|
14
14
|
const raw = typeof process === 'undefined' ? undefined : process.env.FOUGERE_LOG_LEVEL;
|
|
15
|
+
|
|
15
16
|
return raw !== undefined && raw in LEVELS ? (raw as LogLevel) : undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -51,8 +52,10 @@ function supportsColor(): boolean {
|
|
|
51
52
|
if (typeof process !== 'undefined') {
|
|
52
53
|
if (process.env.NO_COLOR) return false;
|
|
53
54
|
if (process.env.FORCE_COLOR === '1') return true;
|
|
55
|
+
|
|
54
56
|
return process.stdout?.isTTY === true;
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
return false;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -62,6 +65,7 @@ function stamp(at: number | Date): string {
|
|
|
62
65
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
63
66
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
64
67
|
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
68
|
+
|
|
65
69
|
return `${h}:${m}:${s}.${ms}`;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -92,7 +96,7 @@ export class Logger {
|
|
|
92
96
|
level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
|
|
93
97
|
};
|
|
94
98
|
|
|
95
|
-
this.carry?.push(record);
|
|
99
|
+
this.carry?.push({ ...record, args: args.map(objectOf) });
|
|
96
100
|
|
|
97
101
|
// The console ALWAYS, whoever else took the line. Skipping it once a destination
|
|
98
102
|
// existed made `calls()` — a devtools ring that prints nothing — silence the
|
|
@@ -103,6 +107,10 @@ export class Logger {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/** A line travels as a fact, where `args` states `json()` — an object. */
|
|
111
|
+
const objectOf = (arg: unknown): object =>
|
|
112
|
+
typeof arg === 'object' && arg !== null && !Array.isArray(arg) ? arg : { value: arg };
|
|
113
|
+
|
|
106
114
|
export function formatted(
|
|
107
115
|
record: Rendered,
|
|
108
116
|
color = supportsColor(),
|