@fougere/core 0.5.0-alpha.0 → 0.6.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/Emissions.d.ts +2 -2
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +1 -1
- package/dist/boot/HandlerFacade.d.ts +1 -1
- package/dist/boot/HandlerFacade.d.ts.map +1 -1
- package/dist/boot/HandlerFacade.js +1 -1
- package/dist/boot/HandlerFacade.js.map +1 -1
- package/dist/boot/auth.d.ts +6 -6
- package/dist/boot/auth.d.ts.map +1 -1
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/boot.d.ts.map +1 -1
- package/dist/boot/boot.js +1 -1
- package/dist/boot/boot.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +47 -38
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts +21 -0
- package/dist/boot/card.d.ts.map +1 -0
- package/dist/boot/card.js +100 -0
- package/dist/boot/card.js.map +1 -0
- package/dist/boot/frame.d.ts +3 -3
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +7 -7
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/hosted.d.ts +8 -2
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +10 -10
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/ownership.d.ts +7 -5
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +9 -14
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/ports.d.ts +2 -2
- package/dist/boot/ports.d.ts.map +1 -1
- package/dist/boot/ports.js +1 -1
- package/dist/boot/runner.d.ts +28 -0
- package/dist/boot/runner.d.ts.map +1 -0
- package/dist/boot/runner.js +26 -0
- package/dist/boot/runner.js.map +1 -0
- package/dist/boot/seed.d.ts +2 -2
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +5 -5
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +2 -1
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts +4 -4
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +17 -19
- package/dist/boot/together.js.map +1 -1
- package/dist/boot/types.d.ts +12 -11
- package/dist/boot/types.d.ts.map +1 -1
- package/dist/contract/CallLog.d.ts +2 -2
- package/dist/contract/CallLog.d.ts.map +1 -1
- package/dist/contract/RouteAddress.d.ts +2 -0
- package/dist/contract/RouteAddress.d.ts.map +1 -1
- package/dist/contract/RouteAddress.js.map +1 -1
- package/dist/{scan/declare.d.ts → declare.d.ts} +3 -3
- package/dist/declare.d.ts.map +1 -0
- package/dist/{scan/declare.js → declare.js} +3 -3
- package/dist/declare.js.map +1 -0
- package/dist/{scan → descriptor}/Fronds.d.ts +6 -7
- package/dist/descriptor/Fronds.d.ts.map +1 -0
- package/dist/{scan → descriptor}/Fronds.js +6 -7
- package/dist/descriptor/Fronds.js.map +1 -0
- package/dist/{scan → descriptor}/frond.d.ts +7 -47
- package/dist/descriptor/frond.d.ts.map +1 -0
- package/dist/{scan → descriptor}/frond.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts +5 -2
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +48 -3
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/DispatchEvent.d.ts +1 -1
- package/dist/dispatch/DispatchEvent.d.ts.map +1 -1
- package/dist/dispatch/PresenterArgumentResolver.d.ts +1 -1
- package/dist/dispatch/PresenterArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/PresenterArgumentResolver.js +1 -1
- package/dist/dispatch/Route.d.ts +1 -2
- package/dist/dispatch/Route.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts +2 -2
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +7 -7
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/effective-operation.d.ts +29 -3
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +52 -1
- package/dist/effective-operation.js.map +1 -1
- package/dist/emit.d.ts +1 -1
- package/dist/frond-config.d.ts +2 -2
- package/dist/graph.d.ts +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/imports.d.ts +1 -1
- package/dist/imports.d.ts.map +1 -1
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -2
- package/dist/node.js.map +1 -1
- package/dist/prefab/collector.d.ts +2 -2
- package/dist/prefab/collector.js +2 -2
- package/dist/prefab/crud.d.ts +5 -5
- package/dist/prefab/crud.d.ts.map +1 -1
- package/dist/prefab/crud.js +12 -12
- package/dist/prefab/crud.js.map +1 -1
- package/dist/prefab/mirror.d.ts +5 -5
- package/dist/prefab/mirror.d.ts.map +1 -1
- package/dist/prefab/mirror.js +5 -5
- package/dist/prefab/mirror.js.map +1 -1
- package/dist/prefab/presenter.d.ts +2 -2
- package/dist/prefab/presenter.js +2 -2
- package/dist/prefab/repository.d.ts +14 -14
- package/dist/prefab/repository.d.ts.map +1 -1
- package/dist/prefab/repository.js +19 -19
- package/dist/prefab/repository.js.map +1 -1
- package/dist/rows.d.ts +40 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +146 -0
- package/dist/rows.js.map +1 -0
- package/dist/scan/conventions.d.ts +8 -12
- package/dist/scan/conventions.d.ts.map +1 -1
- package/dist/scan/conventions.js +0 -13
- package/dist/scan/conventions.js.map +1 -1
- package/dist/scan/emit.d.ts +1 -1
- package/dist/scan/emit.d.ts.map +1 -1
- package/dist/scan/emit.js +1 -1
- package/dist/scan/emit.js.map +1 -1
- package/dist/scan/handler-parser.d.ts +11 -56
- package/dist/scan/handler-parser.d.ts.map +1 -1
- package/dist/scan/handler-parser.js +2 -2
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/result.d.ts +41 -0
- package/dist/scan/result.d.ts.map +1 -0
- package/dist/scan/result.js +2 -0
- package/dist/scan/result.js.map +1 -0
- package/dist/scan/scanner.d.ts +18 -1
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +34 -17
- package/dist/scan/scanner.js.map +1 -1
- package/dist/scan/statement.d.ts +1 -1
- package/dist/scan/statement.d.ts.map +1 -1
- package/dist/scan/statement.js.map +1 -1
- package/dist/{orm.d.ts → storage.d.ts} +98 -16
- package/dist/storage.d.ts.map +1 -0
- package/dist/{orm.js → storage.js} +53 -5
- package/dist/storage.js.map +1 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +2 -2
- package/dist/verify.js.map +1 -1
- package/dist/{boot → wire}/binding.d.ts +2 -10
- package/dist/wire/binding.d.ts.map +1 -0
- package/dist/{boot → wire}/binding.js +0 -52
- package/dist/wire/binding.js.map +1 -0
- package/dist/wire/call.d.ts +2 -40
- package/dist/wire/call.d.ts.map +1 -1
- package/dist/wire/call.js +0 -125
- package/dist/wire/call.js.map +1 -1
- package/dist/wire/operation.d.ts +5 -31
- package/dist/wire/operation.d.ts.map +1 -1
- package/dist/wire/operation.js +0 -51
- package/dist/wire/operation.js.map +1 -1
- package/dist/wire/signature.d.ts +58 -0
- package/dist/wire/signature.d.ts.map +1 -0
- package/dist/wire/signature.js +2 -0
- package/dist/wire/signature.js.map +1 -0
- package/package.json +3 -3
- package/src/boot/Emissions.ts +2 -2
- package/src/boot/HandlerFacade.ts +3 -3
- package/src/boot/auth.ts +6 -6
- package/src/boot/boot.ts +3 -3
- package/src/boot/bootstrap.ts +50 -39
- package/src/boot/card.ts +113 -0
- package/src/boot/frame.ts +7 -7
- package/src/boot/hosted.ts +11 -14
- package/src/boot/ownership.ts +14 -15
- package/src/boot/ports.ts +2 -2
- package/src/boot/runner.ts +34 -0
- package/src/boot/seed.ts +6 -6
- package/src/boot/statement-drift.ts +5 -4
- package/src/boot/together.ts +24 -25
- package/src/boot/types.ts +12 -11
- package/src/contract/CallLog.ts +2 -2
- package/src/contract/RouteAddress.ts +3 -0
- package/src/{scan/declare.ts → declare.ts} +5 -5
- package/src/{scan → descriptor}/Fronds.ts +6 -7
- package/src/{scan → descriptor}/frond.ts +7 -48
- package/src/dispatch/ArgumentResolver.ts +53 -7
- package/src/dispatch/DispatchEvent.ts +1 -1
- package/src/dispatch/PresenterArgumentResolver.ts +2 -2
- package/src/dispatch/Route.ts +1 -3
- package/src/dispatch/StorageGuard.ts +7 -7
- package/src/effective-operation.ts +63 -11
- package/src/emit.ts +1 -1
- package/src/frond-config.ts +2 -2
- package/src/graph.ts +1 -1
- package/src/imports.ts +1 -1
- package/src/index.ts +15 -13
- package/src/node.ts +2 -2
- package/src/prefab/collector.ts +2 -2
- package/src/prefab/crud.ts +18 -18
- package/src/prefab/mirror.ts +8 -8
- package/src/prefab/presenter.ts +2 -2
- package/src/prefab/repository.ts +32 -32
- package/src/rows.ts +155 -0
- package/src/scan/conventions.ts +0 -18
- package/src/scan/emit.ts +3 -5
- package/src/scan/handler-parser.ts +14 -67
- package/src/scan/result.ts +42 -0
- package/src/scan/scanner.ts +44 -21
- package/src/scan/statement.ts +2 -1
- package/src/{orm.ts → storage.ts} +126 -17
- package/src/verify.ts +3 -3
- package/src/{boot → wire}/binding.ts +2 -66
- package/src/wire/call.ts +1 -129
- package/src/wire/operation.ts +5 -62
- package/src/wire/signature.ts +59 -0
- package/dist/boot/binding.d.ts.map +0 -1
- package/dist/boot/binding.js.map +0 -1
- package/dist/orm.d.ts.map +0 -1
- package/dist/orm.js.map +0 -1
- package/dist/scan/Fronds.d.ts.map +0 -1
- package/dist/scan/Fronds.js.map +0 -1
- package/dist/scan/declare.d.ts.map +0 -1
- package/dist/scan/declare.js.map +0 -1
- package/dist/scan/frond.d.ts.map +0 -1
- /package/dist/{scan → descriptor}/frond.js +0 -0
package/src/boot/boot.ts
CHANGED
|
@@ -32,12 +32,12 @@ interface BootOptions {
|
|
|
32
32
|
*/
|
|
33
33
|
onEmit?: CreateAppOptions['onEmit'];
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* storage setup — returns the storage handle (db), an storageFactory, and its two halves.
|
|
36
36
|
* The `db` value is forwarded to the auth provider via AuthContext when `auth` is set.
|
|
37
37
|
*/
|
|
38
38
|
db?: (config: FougereConfig) => {
|
|
39
39
|
db?: unknown;
|
|
40
|
-
|
|
40
|
+
storageFactory: CreateAppOptions['storageFactory'];
|
|
41
41
|
/**
|
|
42
42
|
* Bring the schema up to date — an extension's `up`, because it runs after the
|
|
43
43
|
* container. It used to be called `afterBoot`, a word that also meant the host's
|
|
@@ -88,7 +88,7 @@ export async function boot(options: BootOptions): Promise<App> {
|
|
|
88
88
|
// handed the answer and reaches for nothing.
|
|
89
89
|
scan: await scanProject(root, options.fronds, config.conventions),
|
|
90
90
|
createContainer: options.createContainer,
|
|
91
|
-
|
|
91
|
+
storageFactory: dbSetup?.storageFactory,
|
|
92
92
|
db: dbSetup?.db,
|
|
93
93
|
auth: config.auth,
|
|
94
94
|
remotes: options.remotes,
|
package/src/boot/bootstrap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Container } from '@fougere/container';
|
|
2
2
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
3
|
-
import type { EntityEntry, HandlerEntry, PresenterEntry } from '../
|
|
3
|
+
import type { EntityEntry, HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
|
|
4
4
|
import { hostedBy } from './hosted.js';
|
|
5
5
|
import type { AuthRuntime } from './auth.js';
|
|
6
6
|
import type { CreateAppOptions, App } from './types.js';
|
|
@@ -12,7 +12,7 @@ import { registerFrames } from './together.js';
|
|
|
12
12
|
import { Emissions } from './Emissions.js';
|
|
13
13
|
import { HandlerFacade } from './HandlerFacade.js';
|
|
14
14
|
import { targetOf } from '../prefab/prefab.js';
|
|
15
|
-
import { ownersOf,
|
|
15
|
+
import { ownersOf, refuseStorageInUserCode, refuseCrudOnOwned } from './ownership.js';
|
|
16
16
|
import type { OperationContract, OperationsMap } from '../wire/operation.js';
|
|
17
17
|
import {
|
|
18
18
|
resolveEffectiveOperations,
|
|
@@ -22,10 +22,11 @@ import { StorageGuard } from '../dispatch/StorageGuard.js';
|
|
|
22
22
|
import { portBindings } from './ports.js';
|
|
23
23
|
import { InFlight } from '../dispatch/InFlight.js';
|
|
24
24
|
// The keys, each read from where its concept is declared — never respelled here.
|
|
25
|
-
import { facadeKeyOf, contractsKeyOf,
|
|
25
|
+
import { facadeKeyOf, contractsKeyOf, type RpcAnswer } from '../wire/call.js';
|
|
26
|
+
import { identityCardOf } from './card.js';
|
|
26
27
|
import { AppLifecycle } from './AppLifecycle.js';
|
|
27
28
|
import { repositoryKeyOf } from '../prefab/repository.js';
|
|
28
|
-
import {
|
|
29
|
+
import { storageKeyOf } from '../storage.js';
|
|
29
30
|
import { presenterKeyOf } from '../prefab/presenter.js';
|
|
30
31
|
import { collectorKeyOf } from '../prefab/collector.js';
|
|
31
32
|
import { RouteAddress } from '../contract/RouteAddress.js';
|
|
@@ -112,6 +113,16 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
112
113
|
// no builtin and a Worker can run what it builds.
|
|
113
114
|
const scanStart = performance.now();
|
|
114
115
|
const { fronds, diagnostics } = await hostedBy(options);
|
|
116
|
+
// An app that states nothing AND scans nothing is a mistake — unless something else it
|
|
117
|
+
// declares brings its own entities, which an auth provider does. Refused here and not in
|
|
118
|
+
// `hostedBy`, which is handed the frond sources and cannot see the rest of the app. The
|
|
119
|
+
// condition is the KEYS, not the count: a scan that found nothing is an ordinary answer.
|
|
120
|
+
if (!options.fronds && !options.scan && !options.auth) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
123
|
+
+ 'Neither was given, and nothing else declares entities of its own.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
115
126
|
const operationModel = resolveEffectiveOperations(fronds, {
|
|
116
127
|
diagnostics,
|
|
117
128
|
remotes: options.remotes,
|
|
@@ -152,11 +163,11 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
152
163
|
|
|
153
164
|
// Auth runtime — built once from the lazy AuthConfig produced by a provider factory
|
|
154
165
|
// (e.g. betterAuth({...})) in fougere.config.ts. The provider receives our db +
|
|
155
|
-
//
|
|
166
|
+
// storageFactory so all auth writes flow through Storage.
|
|
156
167
|
let authRuntime: AuthRuntime | undefined;
|
|
157
168
|
if (options.auth) {
|
|
158
|
-
if (!options.
|
|
159
|
-
throw new Error('createApp: `auth` is set but `
|
|
169
|
+
if (!options.storageFactory) {
|
|
170
|
+
throw new Error('createApp: `auth` is set but `storageFactory` is missing — auth providers need it to back their adapter.');
|
|
160
171
|
}
|
|
161
172
|
if (options.db === undefined) {
|
|
162
173
|
throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
|
|
@@ -164,7 +175,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
164
175
|
log.info('initializing auth runtime');
|
|
165
176
|
authRuntime = await options.auth.create({
|
|
166
177
|
db: options.db,
|
|
167
|
-
|
|
178
|
+
storageFactory: options.storageFactory,
|
|
168
179
|
});
|
|
169
180
|
log.info(`auth ready — mounted at ${authRuntime.basePath}`);
|
|
170
181
|
}
|
|
@@ -254,14 +265,14 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
254
265
|
// `reads:` is what makes a cross-source reader exist here, and the list IS its
|
|
255
266
|
// environment — a source holding none of these is never opened. Registered under
|
|
256
267
|
// the type's own name, which is the key `depKeyOf` already derives for a plain
|
|
257
|
-
// parameter: `constructor(private
|
|
268
|
+
// parameter: `constructor(private reads: Reads)` and nothing else to say.
|
|
258
269
|
// Declaring `reads:` with nothing to build the reader is a boot that ignores a
|
|
259
|
-
// clause: the handler asking for `
|
|
270
|
+
// clause: the handler asking for `Reads` then dies at its first call, on a
|
|
260
271
|
// container message that names neither the clause nor what is missing.
|
|
261
272
|
if (frond.reads?.length && !options.sourcesFactory) {
|
|
262
273
|
frondLog.warn(
|
|
263
274
|
`[reads] ${frond.reads.join(', ')} — declared in frond.config.ts, but this boot passes no `
|
|
264
|
-
+ '`sourcesFactory`, so no reader is registered and a handler asking for `
|
|
275
|
+
+ '`sourcesFactory`, so no reader is registered and a handler asking for `Reads` will fail '
|
|
265
276
|
+ 'at its first call. Pass one (`@fougere/adapter-duckdb`), or drop the clause.',
|
|
266
277
|
);
|
|
267
278
|
}
|
|
@@ -280,14 +291,14 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
280
291
|
+ 'so a query naming one would find no table. Check the spelling, or the entity file.',
|
|
281
292
|
);
|
|
282
293
|
}
|
|
283
|
-
scope.registerValue('
|
|
294
|
+
scope.registerValue('Reads', await options.sourcesFactory(named, frond.name));
|
|
284
295
|
frondLog.debug(`cross-source reader over ${named.length} entit(ies)`);
|
|
285
296
|
}
|
|
286
297
|
|
|
287
298
|
// Who owns what, and the rule that makes owning mean something. Before anything is
|
|
288
299
|
// registered, so a bad line is named by this refusal rather than by the container's.
|
|
289
300
|
const owners = ownersOf(frond.providers);
|
|
290
|
-
|
|
301
|
+
refuseStorageInUserCode(frond, owners, (entity) => entityByName.has(entity));
|
|
291
302
|
refuseCrudOnOwned(frond, owners);
|
|
292
303
|
|
|
293
304
|
for (const provider of frond.providers) {
|
|
@@ -306,28 +317,28 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
306
317
|
frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map((p) => p.ctor.name).join(', ')}`);
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
// Register
|
|
310
|
-
// When a handler declares Crud(Entity, Output), scope the
|
|
311
|
-
if (options.
|
|
320
|
+
// Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
|
|
321
|
+
// When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
|
|
322
|
+
if (options.storageFactory) {
|
|
312
323
|
for (const entity of frond.entities) {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
324
|
+
const key = storageKeyOf(entity.name);
|
|
325
|
+
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
315
326
|
|
|
316
327
|
// Check if the default handler (no surface) declares an output override
|
|
317
328
|
const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
|
|
318
329
|
const outputSchema = defaultHandler?.outputOverride ?? (defaultHandler?.ctor as any)?.__output;
|
|
319
330
|
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
320
|
-
?
|
|
321
|
-
:
|
|
331
|
+
? baseStorage.output(outputSchema)
|
|
332
|
+
: baseStorage;
|
|
322
333
|
|
|
323
334
|
// Storage is a way out like the client surface — see `StorageGuard`.
|
|
324
335
|
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(scoped);
|
|
325
|
-
scope.registerValue(
|
|
336
|
+
scope.registerValue(key, guarded);
|
|
326
337
|
|
|
327
338
|
// The default repository IS the guarded port — it already answers every gesture a
|
|
328
339
|
// declared one forwards, so the two forms have the same shape and a handler reads
|
|
329
|
-
// `repo.list()` either way. The wrapper that used to sit here (`{
|
|
330
|
-
// existed to make `repo.
|
|
340
|
+
// `repo.list()` either way. The wrapper that used to sit here (`{ storage: guarded }`)
|
|
341
|
+
// existed to make `repo.storage` true in both, back when `.storage` was the way in.
|
|
331
342
|
//
|
|
332
343
|
// Not registered for an OWNED entity: an aggregate's members are reached through it
|
|
333
344
|
// and nowhere else, and the default would be a second door under a name a handler
|
|
@@ -342,7 +353,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
342
353
|
}
|
|
343
354
|
}
|
|
344
355
|
if (frond.entities.length > 0) {
|
|
345
|
-
frondLog.debug(`${frond.entities.length} entity
|
|
356
|
+
frondLog.debug(`${frond.entities.length} entity storage(s): ${frond.entities.map((e) => e.name).join(', ')}`);
|
|
346
357
|
}
|
|
347
358
|
}
|
|
348
359
|
|
|
@@ -357,7 +368,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
357
368
|
entityByName,
|
|
358
369
|
frondOf,
|
|
359
370
|
hostedHere: (name) => !(options.remotes && name in options.remotes),
|
|
360
|
-
|
|
371
|
+
storageFactory: options.storageFactory,
|
|
361
372
|
sourceOf: options.sourceOf,
|
|
362
373
|
transacted: options.transacted,
|
|
363
374
|
log: frondLog,
|
|
@@ -485,7 +496,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
485
496
|
const facadeKey = facadeKeyOf(handler.address);
|
|
486
497
|
buildFacade(entity, handler, scope, facadeKey);
|
|
487
498
|
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
|
|
488
|
-
+ (entity ? '' : ' — no entity of that name: no
|
|
499
|
+
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
489
500
|
}
|
|
490
501
|
|
|
491
502
|
// The dual, and it stays: a shape that declares no operation answers nothing. Said
|
|
@@ -496,7 +507,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
496
507
|
}
|
|
497
508
|
}
|
|
498
509
|
|
|
499
|
-
// Surface handlers — create sub-scope per surface handler with scoped
|
|
510
|
+
// Surface handlers — create sub-scope per surface handler with scoped storage
|
|
500
511
|
//
|
|
501
512
|
// Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
|
|
502
513
|
// carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
|
|
@@ -506,25 +517,25 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
506
517
|
const entity = frond.entities.find((e) => e.name === handler.address);
|
|
507
518
|
const surfaceScope = scope.createScope();
|
|
508
519
|
|
|
509
|
-
// Register scoped
|
|
520
|
+
// Register scoped storage if output override differs from entity — under the REPOSITORY
|
|
510
521
|
// key, which is what a Crud handler asks for, and under the port's own for a holder
|
|
511
522
|
// that legitimately names it. Registering only the latter left a named surface with
|
|
512
523
|
// no door at all once the façade stopped spelling the storage.
|
|
513
|
-
if (entity && options.
|
|
514
|
-
const
|
|
524
|
+
if (entity && options.storageFactory) {
|
|
525
|
+
const baseStorage = options.storageFactory(entity.entityClass, entity.name);
|
|
515
526
|
const outputSchema = handler.outputOverride ?? (handler.ctor as any).__output;
|
|
516
527
|
const scoped = outputSchema && outputSchema !== entity.entityClass
|
|
517
|
-
?
|
|
518
|
-
:
|
|
528
|
+
? baseStorage.output(outputSchema)
|
|
529
|
+
: baseStorage;
|
|
519
530
|
const guarded = new StorageGuard(entity.entityClass.getFields(), entity.name).guard(scoped);
|
|
520
|
-
surfaceScope.registerValue(
|
|
531
|
+
surfaceScope.registerValue(storageKeyOf(entity.name), guarded);
|
|
521
532
|
surfaceScope.registerValue(repositoryKeyOf(entity.name), guarded);
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
const facadeKey = facadeKeyOf(handler.address, handler.surface);
|
|
525
536
|
buildFacade(entity, handler, surfaceScope, facadeKey);
|
|
526
537
|
frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as any).join(', ')}]`
|
|
527
|
-
+ (entity ? '' : ' — no entity of that name: no
|
|
538
|
+
+ (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
|
|
528
539
|
}
|
|
529
540
|
|
|
530
541
|
// A named surface is closed, so what it contains is a fact worth stating.
|
|
@@ -746,15 +757,15 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
746
757
|
* client-facing door. Both are ways in; an entity that opens none of them still has rows.
|
|
747
758
|
*
|
|
748
759
|
* It resolves through the owning frond's scope, because that is where entity ORMs live:
|
|
749
|
-
* `resolve('
|
|
760
|
+
* `resolve('UserStorage')` reads the ROOT container and never finds one, so callers outside
|
|
750
761
|
* the frond concluded there was no storage. The seed loop did exactly that, and skipped
|
|
751
762
|
* every entity with no façade — the one case its own fallback existed for.
|
|
752
763
|
*/
|
|
753
|
-
const
|
|
764
|
+
const storageFor = (entity: string): unknown | undefined => {
|
|
754
765
|
const owner = fronds.owner(entity);
|
|
755
766
|
if (!owner) return undefined;
|
|
756
767
|
|
|
757
|
-
const key =
|
|
768
|
+
const key = storageKeyOf(entity);
|
|
758
769
|
try {
|
|
759
770
|
return container.resolve<Container>(`frond:${owner.name}`).resolve(key);
|
|
760
771
|
} catch {
|
|
@@ -765,7 +776,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
765
776
|
/**
|
|
766
777
|
* The presenter of an entity, resolved through its owning frond's scope.
|
|
767
778
|
*
|
|
768
|
-
* Same shape as `
|
|
779
|
+
* Same shape as `storageFor`, and it exists for the same reason: without it an adapter
|
|
769
780
|
* has to spell the container key itself. `schema-graphql` did — a hand-written
|
|
770
781
|
* `${Name}Presenter` — and a key respelled elsewhere finds nothing and says nothing
|
|
771
782
|
* the day the convention moves, which is exactly the failure `verify.ts` refuses.
|
|
@@ -799,7 +810,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
|
|
|
799
810
|
operationsFor,
|
|
800
811
|
listensTo: () => emissions.listensTo(),
|
|
801
812
|
deliver: (fact, payload) => emissions.deliver(fact, payload),
|
|
802
|
-
|
|
813
|
+
storageFor,
|
|
803
814
|
presenterFor,
|
|
804
815
|
dispose: release,
|
|
805
816
|
drain,
|
package/src/boot/card.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an app says about ITSELF — the `rpc.discover` answer, built from the app.
|
|
3
|
+
*
|
|
4
|
+
* The shape is `wire/`'s: it crosses a process boundary. Building one needs an App,
|
|
5
|
+
* which is why the function lives here and the shape does not.
|
|
6
|
+
*/
|
|
7
|
+
import { Card } from '@fougere/schema';
|
|
8
|
+
import type { App } from './types.js';
|
|
9
|
+
import { factsAnnouncedBy } from '../emit.js';
|
|
10
|
+
import type { InvocationContext } from '../contract/Invocation.js';
|
|
11
|
+
import { facadeKeyOf, contractsKeyOf, type CardOp, type IdentityCard } from '../wire/call.js';
|
|
12
|
+
import type { OperationContract } from '../wire/operation.js';
|
|
13
|
+
|
|
14
|
+
type AnyFacade = Record<string, (invocation?: InvocationContext) => Promise<unknown>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Serialize what the app hosts, for one audience. Served on `rpc.discover`,
|
|
18
|
+
* cached by callers.
|
|
19
|
+
*
|
|
20
|
+
* Hosting means answering: an entity with no façade declares no operation, so
|
|
21
|
+
* it is not among the DOORS — publishing its shape would hand an anonymous caller
|
|
22
|
+
* the structure of tables it can never reach (the auth tables, typically), and
|
|
23
|
+
* `fougere sync` would rebuild a schema with nothing to call on it.
|
|
24
|
+
*
|
|
25
|
+
* `facts` is the one thing that shape-without-a-door rule does not cover, and the
|
|
26
|
+
* reason is the opposite of a leak: an entity nobody exposed is silent, whereas a
|
|
27
|
+
* fact was explicitly declared to leave.
|
|
28
|
+
*
|
|
29
|
+
* `surface` makes that sentence audience-aware rather than app-wide: a door
|
|
30
|
+
* answers with what IT serves. Nothing falls back to the full façade — under a
|
|
31
|
+
* named surface, an entity with no façade of its own is simply not there.
|
|
32
|
+
*/
|
|
33
|
+
export function identityCardOf(app: App, surface?: string): IdentityCard {
|
|
34
|
+
const declared = app.fronds.schemas();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
fronds: app.fronds.map((frond) => {
|
|
38
|
+
// What the frond answers to, not what it stores. This walked `frond.entities`, so a
|
|
39
|
+
// handler carrying no entity — a health check, a search across shapes — was built,
|
|
40
|
+
// served, and absent from the card: `sync` could not generate its door and a remote
|
|
41
|
+
// consumer had no way to know it existed. The boot has said "pointing at nothing is
|
|
42
|
+
// legal" since handlers became the subject; the card had not caught up.
|
|
43
|
+
const byEntity = new Map(frond.entities.map((entity) => [entity.name, entity]));
|
|
44
|
+
const addresses = [...new Set([
|
|
45
|
+
...frond.entities.map((entity) => entity.name),
|
|
46
|
+
...frond.handlers.map((handler) => handler.address),
|
|
47
|
+
])];
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: frond.name,
|
|
51
|
+
doors: addresses.flatMap((address) => {
|
|
52
|
+
const ops = facadeOps(app, address, surface);
|
|
53
|
+
if (ops.length === 0) return [];
|
|
54
|
+
const entity = byEntity.get(address);
|
|
55
|
+
return [{
|
|
56
|
+
name: address,
|
|
57
|
+
ops,
|
|
58
|
+
// Absent when nothing of that name is stored. A door is still a door.
|
|
59
|
+
...(entity ? { schema: Card.fromSchema(entity.entityClass, address).descriptor } : {}),
|
|
60
|
+
}];
|
|
61
|
+
}),
|
|
62
|
+
/**
|
|
63
|
+
* What leaves on its own — the same list on every surface, deliberately.
|
|
64
|
+
*
|
|
65
|
+
* A door has an audience; a fact does not. `Emit<T>` names a subject and *the
|
|
66
|
+
* number of readers is not the emitter's business* (`emit.ts`), so narrowing this
|
|
67
|
+
* by `surface` would invent an axis the primitive refuses. The consequence is
|
|
68
|
+
* stated rather than hidden: a fact's SHAPE is readable by anyone who can read the
|
|
69
|
+
* card at all. What must not be published must not be announced.
|
|
70
|
+
*/
|
|
71
|
+
facts: factsAnnouncedBy(frond.handlers).map((name) => {
|
|
72
|
+
const entityClass = declared.get(name);
|
|
73
|
+
return { name, ...(entityClass ? { schema: Card.fromSchema(entityClass, name).descriptor } : {}) };
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function facadeOps(app: App, entityName: string, surface?: string): CardOp[] {
|
|
81
|
+
let facade: AnyFacade;
|
|
82
|
+
try {
|
|
83
|
+
facade = app.container.resolve<AnyFacade>(facadeKeyOf(entityName, surface));
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The façade is the list of names; the model is the resolved terms.
|
|
89
|
+
const effective = app.operationsFor(entityName, surface);
|
|
90
|
+
if (!effective) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Facade '${facadeKeyOf(entityName, surface)}' exists without an effective operation table.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Object.keys(facade).map((name) => {
|
|
97
|
+
const contract = effective.get(name);
|
|
98
|
+
if (!contract) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Facade '${facadeKeyOf(entityName, surface)}' serves '${name}' without an effective contract.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
...(contract?.description && { description: contract.description }),
|
|
107
|
+
...(contract?.input && { input: Card.fromSchema(contract.input, name).descriptor }),
|
|
108
|
+
...(contract?.output && { output: Card.fromSchema(contract.output, name).descriptor }),
|
|
109
|
+
...(contract?.cardinality && { cardinality: contract.cardinality }),
|
|
110
|
+
kind: contract.kind,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/boot/frame.ts
CHANGED
|
@@ -52,7 +52,7 @@ const pick = (row: Record<string, unknown>, keys: readonly string[]): Record<str
|
|
|
52
52
|
*
|
|
53
53
|
* The inverse of an upsert is derivable exactly when the conflict is the primary key: read
|
|
54
54
|
* the rows by key first, then restore the ones that were there and delete the ones that
|
|
55
|
-
* were not. `
|
|
55
|
+
* were not. `SqlStorage` spells `onConflict(pk)` on SQLite and Postgres, so that holds —
|
|
56
56
|
* but MySQL's `onDuplicateKeyUpdate` fires on ANY unique constraint, and a row that
|
|
57
57
|
* conflicts on a unique email while carrying a different key would be restored under a key
|
|
58
58
|
* the write never touched.
|
|
@@ -79,15 +79,15 @@ function refuseAmbiguousUpsert(entity: string, gesture: string): never {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* The
|
|
82
|
+
* The storage a member is handed inside a compensated frame: the same port, writing the same
|
|
83
83
|
* rows, leaving an inverse behind each time.
|
|
84
84
|
*
|
|
85
|
-
* `Object.create` for the same reason `StorageGuard` uses it — the
|
|
85
|
+
* `Object.create` for the same reason `StorageGuard` uses it — the storage keeps every gesture
|
|
86
86
|
* it had, including the ones this knows nothing about.
|
|
87
87
|
*/
|
|
88
|
-
export function recording<T extends object>(
|
|
89
|
-
const base =
|
|
90
|
-
if (typeof base.create !== 'function' || typeof base.update !== 'function') return
|
|
88
|
+
export function recording<T extends object>(storage: T, entity: string, fields: Fields, journal: Undo[]): T {
|
|
89
|
+
const base = storage as unknown as Undoable;
|
|
90
|
+
if (typeof base.create !== 'function' || typeof base.update !== 'function') return storage;
|
|
91
91
|
|
|
92
92
|
const key = FieldSet.of(fields).primary;
|
|
93
93
|
if (!key) {
|
|
@@ -97,7 +97,7 @@ export function recording<T extends object>(orm: T, entity: string, fields: Fiel
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const recorded = Object.create(
|
|
100
|
+
const recorded = Object.create(storage) as T & Undoable;
|
|
101
101
|
|
|
102
102
|
recorded.create = async function (...args) {
|
|
103
103
|
const row = await base.create.apply(this, args);
|
package/src/boot/hosted.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* because the point of stating is to decide. Empty is what "I named none" looks like, and
|
|
17
17
|
* `frond()` builds every member, so an empty one is filled rather than enforced.
|
|
18
18
|
*/
|
|
19
|
-
import type { FrondDescriptor
|
|
20
|
-
import {
|
|
19
|
+
import type { FrondDescriptor } from '../descriptor/frond.js';
|
|
20
|
+
import type { ScanResult } from '../scan/result.js';
|
|
21
|
+
import { Fronds } from '../descriptor/Fronds.js';
|
|
21
22
|
|
|
22
23
|
/** Where an app's fronds come from — either, or both. */
|
|
23
24
|
export interface HostedSources {
|
|
@@ -52,26 +53,22 @@ function completed(stated: FrondDescriptor, found: FrondDescriptor | undefined):
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* What the app hosts, and what the scan — if it ran — could not do.
|
|
58
|
+
*
|
|
59
|
+
* Neither key is EMPTY rather than refused: whether an app with no frond is legitimate
|
|
60
|
+
* depends on what else it declares, and this function is handed none of that.
|
|
61
|
+
*/
|
|
56
62
|
export async function hostedBy(sources: HostedSources): Promise<ScanResult> {
|
|
57
63
|
const scanned = sources.scan
|
|
58
64
|
? await (typeof sources.scan === 'function' ? sources.scan() : sources.scan)
|
|
59
65
|
: undefined;
|
|
60
66
|
|
|
61
|
-
if (!sources.fronds) {
|
|
62
|
-
if (!scanned) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
'createApp needs `fronds:` (what this app states) or `scan:` (what a scanner found). '
|
|
65
|
-
+ 'Neither was given, so nothing is hosted.',
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return scanned;
|
|
70
|
-
}
|
|
67
|
+
if (!sources.fronds) return scanned ?? { fronds: Fronds.hosting([]), diagnostics: [] };
|
|
71
68
|
|
|
72
69
|
const found = scanned?.fronds ?? [];
|
|
73
70
|
const stated = sources.fronds.map((f) => completed(f, found.find((s) => s.name === f.name)));
|
|
74
71
|
const unstated = found.filter((f) => !sources.fronds!.some((s) => s.name === f.name));
|
|
75
72
|
|
|
76
|
-
return { fronds: Fronds.
|
|
73
|
+
return { fronds: Fronds.hosting([...stated, ...unstated]), diagnostics: scanned?.diagnostics ?? [] };
|
|
77
74
|
}
|
package/src/boot/ownership.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { lowerFirst } from '@fougere/schema';
|
|
2
|
-
import type { FrondDescriptor, ProviderEntry } from '../
|
|
2
|
+
import type { FrondDescriptor, ProviderEntry } from '../descriptor/frond.js';
|
|
3
3
|
import { targetOf } from '../prefab/prefab.js';
|
|
4
4
|
import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
|
|
5
|
-
import {
|
|
5
|
+
import { entityOfStorageKey } from '../storage.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Who owns an entity's storage.
|
|
@@ -37,7 +37,7 @@ export function ownersOf(providers: readonly ProviderEntry[]): Map<string, strin
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* The entities a class was BUILT ON — what a prefab may legitimately be handed the storage
|
|
40
|
-
* of. Empty for a plain service, which is why one cannot ask for
|
|
40
|
+
* of. Empty for a plain service, which is why one cannot ask for a storage at all.
|
|
41
41
|
*/
|
|
42
42
|
function builtOn(ctor: unknown): string[] {
|
|
43
43
|
const owned = ownedBy(ctor);
|
|
@@ -47,19 +47,25 @@ function builtOn(ctor: unknown): string[] {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* `
|
|
50
|
+
* `Storage<E>` is not a word of the user's vocabulary — `<E>Repository` is the one way in.
|
|
51
51
|
*
|
|
52
52
|
* A door judges and projects; a HOLDER keeps the storage. So the rule is not about which
|
|
53
53
|
* directory a file sits in but about which of the two a class is: a handler, a presenter and
|
|
54
|
-
* a collector may never name
|
|
55
|
-
* was built on — which is `Repository` and `Mirror`, without either being named here.
|
|
54
|
+
* a collector may never name the port, and a provider may name only the storage of what its
|
|
55
|
+
* prefab was built on — which is `Repository` and `Mirror`, without either being named here.
|
|
56
56
|
*
|
|
57
57
|
* What it buys beyond one word instead of two: an entity that becomes owned later needs no
|
|
58
58
|
* call site to move, because none of them ever spelled the storage.
|
|
59
59
|
*
|
|
60
|
+
* `known` answers what the key's suffix cannot: `FileStorage` is an ordinary class name.
|
|
61
|
+
*
|
|
60
62
|
* Refused at boot rather than reported by `verify`, which nothing calls at startup.
|
|
61
63
|
*/
|
|
62
|
-
export function
|
|
64
|
+
export function refuseStorageInUserCode(
|
|
65
|
+
frond: FrondDescriptor,
|
|
66
|
+
owners: Map<string, string>,
|
|
67
|
+
known: (entity: string) => boolean,
|
|
68
|
+
): void {
|
|
63
69
|
const doors = [
|
|
64
70
|
...frond.handlers.map((h) => ({ ...h, kind: 'handler' })),
|
|
65
71
|
...frond.presenters.map((p) => ({ ...p, kind: 'presenter' })),
|
|
@@ -70,7 +76,7 @@ export function refuseOrmInUserCode(frond: FrondDescriptor, owners: Map<string,
|
|
|
70
76
|
for (const decl of [...doors, ...holders]) {
|
|
71
77
|
const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
|
|
72
78
|
for (const dep of decl.deps) {
|
|
73
|
-
const entity =
|
|
79
|
+
const entity = entityOfStorageKey(dep, known);
|
|
74
80
|
if (!entity) continue;
|
|
75
81
|
|
|
76
82
|
const owner = owners.get(entity);
|
|
@@ -128,10 +134,3 @@ export function refuseCrudOnOwned(frond: FrondDescriptor, owners: Map<string, st
|
|
|
128
134
|
);
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
|
-
|
|
132
|
-
/** `'PostOrm'` → `'post'`, and nothing for a key that is not one. */
|
|
133
|
-
function entityOfOrmKey(dep: string): string | undefined {
|
|
134
|
-
if (!dep.endsWith('Orm') || dep === 'Orm') return undefined;
|
|
135
|
-
const entity = lowerFirst(dep.slice(0, -'Orm'.length));
|
|
136
|
-
return ormKeyOf(entity) === dep ? entity : undefined;
|
|
137
|
-
}
|
package/src/boot/ports.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderEntry } from '../
|
|
1
|
+
import type { ProviderEntry } from '../descriptor/frond.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A port is a class something already answers under. Nothing declares one — the
|
|
@@ -6,7 +6,7 @@ import type { ProviderEntry } from '../scan/frond.js';
|
|
|
6
6
|
* registration, and so is `class MyLogger extends Logger` over a builtin.
|
|
7
7
|
*
|
|
8
8
|
* This is the fourth reading of the rule the other three already apply: the type
|
|
9
|
-
* names the SUBJECT (`
|
|
9
|
+
* names the SUBJECT (`Storage<E>`, `Emit<F>`, `Facade<H>`) and the container
|
|
10
10
|
* holds the realization. Providers were the one case where the type named the
|
|
11
11
|
* realization instead, so `constructor(private payment: Payment)` resolved to the
|
|
12
12
|
* base class and a handler got an object whose method did not exist.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Transport built from an app — the two ways a call enters this process.
|
|
3
|
+
*
|
|
4
|
+
* `Transport` is `wire/`'s shape; making one needs an App and the transport entry.
|
|
5
|
+
*/
|
|
6
|
+
import type { App } from './types.js';
|
|
7
|
+
import type { Transport } from '../wire/call.js';
|
|
8
|
+
import { createTransportEntry } from '../entry/TransportEntry.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the local runner — the reference realization of Transport.
|
|
12
|
+
*
|
|
13
|
+
* Resolves strictly from the app's own container: a call that lands here is
|
|
14
|
+
* judged here. A miss is a typed NOT_FOUND, never a forward to another remote.
|
|
15
|
+
*/
|
|
16
|
+
export function createLocalRunner(app: App, surface?: string): Transport {
|
|
17
|
+
return createTransportEntry(app.local, surface);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the app runner — same judgment, but resolution follows the app's
|
|
22
|
+
* topology: local façades and remote doublures alike. This is the runner
|
|
23
|
+
* an app's own entry points (browser endpoint, bridges) stand on.
|
|
24
|
+
*
|
|
25
|
+
* `surface` is the audience this runner serves, and it belongs to the DOOR that
|
|
26
|
+
* builds the runner, never to the call: a caller cannot name its own audience
|
|
27
|
+
* any more than it can name its own identity (`state` is stamped server-side
|
|
28
|
+
* for the same reason). Reaching the admin door IS the proof. So `FrondCall`
|
|
29
|
+
* gains nothing and the wire format gains nothing — a remote frond's audience
|
|
30
|
+
* is simply which URL `remotes:` points at.
|
|
31
|
+
*/
|
|
32
|
+
export function createAppRunner(app: App, surface?: string): Transport {
|
|
33
|
+
return createTransportEntry(app, surface);
|
|
34
|
+
}
|
package/src/boot/seed.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Role } from '@fougere/schema';
|
|
2
2
|
import { facadeKeyOf } from '../wire/call.js';
|
|
3
|
-
import type { FrondDescriptor, SeedEntry, SeedFactory } from '../
|
|
3
|
+
import type { FrondDescriptor, SeedEntry, SeedFactory } from '../descriptor/frond.js';
|
|
4
4
|
import type { App } from './types.js';
|
|
5
5
|
import type { Extension } from './AppLifecycle.js';
|
|
6
6
|
|
|
@@ -73,7 +73,7 @@ interface SeedDoor {
|
|
|
73
73
|
* Plant a set of seeds, in the order given. Reports what it did, one line per entity.
|
|
74
74
|
*
|
|
75
75
|
* The one seeding loop. `boot()` had one and the Nuxt module generated a second into its
|
|
76
|
-
* Nitro plugin, which had drifted: no
|
|
76
|
+
* Nitro plugin, which had drifted: no storage fallback, so an entity with no façade was
|
|
77
77
|
* skipped there and planted here. Two answers to "how does a row get in at boot" is one
|
|
78
78
|
* too many, and the second was the one running in the browser.
|
|
79
79
|
*/
|
|
@@ -88,7 +88,7 @@ export async function runSeeds(
|
|
|
88
88
|
|
|
89
89
|
const door = doorFor(app, seed.entityName);
|
|
90
90
|
if (!door) {
|
|
91
|
-
report(` ${seed.entityName}: no handler façade nor
|
|
91
|
+
report(` ${seed.entityName}: no handler façade nor storage — skipping seed`);
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -130,12 +130,12 @@ function doorFor(app: App, entityName: string): SeedDoor | undefined {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const storage = app.storageFor(entityName) as
|
|
134
134
|
| { list: () => Promise<unknown[]>; create: (input: unknown) => Promise<unknown> }
|
|
135
135
|
| undefined;
|
|
136
|
-
if (!
|
|
136
|
+
if (!storage) return undefined;
|
|
137
137
|
|
|
138
|
-
return { list: () =>
|
|
138
|
+
return { list: () => storage.list(), write: (item) => storage.create(item) };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* a refusal: the statement is still the author's word, and a boot that ran yesterday must
|
|
14
14
|
* not stop running because a parameter moved. What it must not do is stay quiet.
|
|
15
15
|
*/
|
|
16
|
-
import type { FrondDescriptor, HandlerEntry
|
|
17
|
-
import type {
|
|
18
|
-
import type {
|
|
16
|
+
import type { FrondDescriptor, HandlerEntry } from '../descriptor/frond.js';
|
|
17
|
+
import type { ScanDiagnostic } from '../scan/result.js';
|
|
18
|
+
import type { BindingPlan } from '../wire/binding.js';
|
|
19
|
+
import type { Signature } from '../wire/signature.js';
|
|
19
20
|
|
|
20
21
|
/** The parameters a stated binding names, against the ones the method declares. */
|
|
21
|
-
function namesDisagreeing(stated: BindingPlan, signature:
|
|
22
|
+
function namesDisagreeing(stated: BindingPlan, signature: Signature): string[] {
|
|
22
23
|
const declared = new Set(signature.params.map((p: { name: string }) => p.name));
|
|
23
24
|
|
|
24
25
|
return stated.map((b) => b.name).filter((name) => !declared.has(name));
|