@fougere/core 0.12.0-alpha.0 → 0.12.2-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.map +1 -1
- package/dist/EffectiveOperationModel.js +20 -1
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/Source.js +2 -2
- package/dist/Source.js.map +1 -1
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js.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 +3 -2
- 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 +8 -2
- 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 +8 -2
- package/dist/boot/remote.js.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 +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- 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.map +1 -1
- package/dist/descriptor/Fronds.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.map +1 -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/OutputView.d.ts.map +1 -1
- package/dist/dispatch/OutputView.js +7 -0
- package/dist/dispatch/OutputView.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +7 -0
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Received.d.ts +3 -0
- package/dist/dispatch/Received.d.ts.map +1 -0
- package/dist/dispatch/Received.js +2 -0
- package/dist/dispatch/Received.js.map +1 -0
- 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/dispatch/decoded.d.ts +19 -0
- package/dist/dispatch/decoded.d.ts.map +1 -0
- package/dist/dispatch/decoded.js +36 -0
- package/dist/dispatch/decoded.js.map +1 -0
- package/dist/entry/facade.d.ts +9 -2
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +16 -7
- package/dist/entry/facade.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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +3 -2
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/CrudOps.d.ts +2 -2
- package/dist/prefab/CrudOps.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/Refreshed.d.ts +7 -2
- package/dist/prefab/Refreshed.d.ts.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/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 +7 -11
- 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 +10 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/AppNext.d.ts +2 -1
- package/dist/wire/AppNext.d.ts.map +1 -1
- package/dist/wire/Facade.d.ts +14 -2
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js +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.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/Page.d.ts +32 -0
- package/dist/wire/Page.d.ts.map +1 -0
- package/dist/wire/Page.js +32 -0
- package/dist/wire/Page.js.map +1 -0
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/Signature.d.ts +7 -0
- package/dist/wire/Signature.d.ts.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 +38 -1
- package/src/FougereConfig.ts +3 -0
- package/src/FrondConfig.ts +2 -0
- package/src/Source.ts +2 -2
- package/src/boot/AppLifecycle.ts +1 -0
- package/src/boot/Emissions.ts +4 -0
- 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 +9 -2
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +8 -1
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +39 -2
- package/src/boot/remote.ts +10 -2
- package/src/boot/seed.ts +1 -1
- 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 +3 -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 +2 -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 +2 -0
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/OutputView.ts +7 -0
- package/src/dispatch/PresenterExecutor.ts +7 -0
- package/src/dispatch/Received.ts +2 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/dispatch/decoded.ts +37 -0
- package/src/entry/facade.ts +21 -9
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +1 -0
- package/src/prefab/CrudConstructor.ts +4 -2
- package/src/prefab/CrudOps.ts +2 -2
- package/src/prefab/MirrorConstructor.ts +1 -1
- package/src/prefab/Refreshed.ts +7 -2
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +15 -10
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +12 -1
- package/src/wire/AppNext.ts +2 -1
- package/src/wire/Facade.ts +14 -2
- package/src/wire/FougereError.ts +3 -1
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/Page.ts +50 -0
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/Signature.ts +7 -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/src/storage/Store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyCreate, applyUpdate,
|
|
1
|
+
import { applyCreate, applyUpdate, Role, type SchemaView } from '@fougere/schema';
|
|
2
2
|
import { comparisonOf, comparisonsIn, type Comparison } from './Comparison.js';
|
|
3
3
|
import type { Storage } from './Storage.js';
|
|
4
4
|
import type { StorageFactory } from './StorageFactory.js';
|
|
@@ -21,7 +21,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
21
21
|
return (entity: SchemaView, name: string): Storage => {
|
|
22
22
|
const fields = entity.getFields();
|
|
23
23
|
const store = open(entity, name);
|
|
24
|
-
const pk = Object.entries(fields).find(([, field]) => Role.of(field).isPrimary)?.[0] ?? 'id';
|
|
24
|
+
const pk = Object.entries(fields).find(([, field]) => Role.of(field).isPrimary())?.[0] ?? 'id';
|
|
25
25
|
// `Storage.findById(id: string)` — but a key can hold a number, and a Map keyed on
|
|
26
26
|
// `1` does not answer `'1'`. SQL never had the question; here the divergence was
|
|
27
27
|
// silent and only on this storage.
|
|
@@ -46,20 +46,18 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
46
46
|
? Object.fromEntries(Object.entries(values).filter(([key]) => selected.has(key)))
|
|
47
47
|
: values);
|
|
48
48
|
|
|
49
|
-
// Same contract as SQL:
|
|
49
|
+
// Same contract as SQL: a row that is already there is UPDATED, so what the write leaves
|
|
50
|
+
// out stays where it was.
|
|
50
51
|
// Named, and not reached through `this`: a caller may have wrapped these gestures,
|
|
51
52
|
// and a derived one that goes back through the front facade is judged twice.
|
|
52
53
|
const upsert = async (input: Partial<Record<string, unknown>>): Promise<Values> => {
|
|
53
|
-
const
|
|
54
|
-
const id =
|
|
54
|
+
const created = applyCreate(fields, applyUpdate(fields, input));
|
|
55
|
+
const id = created[pk] as string | undefined;
|
|
55
56
|
if (id === undefined) throw new Error(`${name}.upsert(): no \`${pk}\` — an upsert needs the key it writes at.`);
|
|
56
57
|
const previous = await store.get(keyOf(id));
|
|
57
|
-
|
|
58
|
-
for (const [key, field] of Object.entries(fields)) {
|
|
59
|
-
if (key === pk || Lifecycle.of(field).stampedOnce) values[key] = previous[key];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
58
|
+
const values = previous ? { ...previous, ...applyUpdate(fields, input) } : created;
|
|
62
59
|
await store.set(keyOf(id), values);
|
|
60
|
+
|
|
63
61
|
return pick(values);
|
|
64
62
|
};
|
|
65
63
|
|
|
@@ -74,10 +72,12 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
74
72
|
},
|
|
75
73
|
async findById(id: string) {
|
|
76
74
|
const values = await store.get(keyOf(id));
|
|
75
|
+
|
|
77
76
|
return values && pick(values);
|
|
78
77
|
},
|
|
79
78
|
async findBy(criteria: Record<string, unknown>) {
|
|
80
79
|
const values = (await store.all()).find((held) => matches(held, criteria));
|
|
80
|
+
|
|
81
81
|
return values && pick(values);
|
|
82
82
|
},
|
|
83
83
|
async findAllBy(criteria: Record<string, unknown>) {
|
|
@@ -90,6 +90,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
90
90
|
const values = await store.get(keyOf(id));
|
|
91
91
|
if (values) found.set(String(id), pick(values));
|
|
92
92
|
}
|
|
93
|
+
|
|
93
94
|
return found;
|
|
94
95
|
},
|
|
95
96
|
// The dual, same contract as SQL: grouped by the value read off the instance.
|
|
@@ -103,11 +104,13 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
103
104
|
const bucket = grouped.get(key);
|
|
104
105
|
if (bucket) bucket.push(pick(values)); else grouped.set(key, [pick(values)]);
|
|
105
106
|
}
|
|
107
|
+
|
|
106
108
|
return grouped;
|
|
107
109
|
},
|
|
108
110
|
upsert,
|
|
109
111
|
async upsertAll(inputs: readonly Partial<Record<string, unknown>>[]) {
|
|
110
112
|
for (const input of inputs) await upsert(input);
|
|
113
|
+
|
|
111
114
|
return inputs.length;
|
|
112
115
|
},
|
|
113
116
|
async create(input: Partial<Record<string, unknown>>) {
|
|
@@ -128,6 +131,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
128
131
|
throw new Error(`${name}.create: '${pk}' ${JSON.stringify(id)} already exists.`);
|
|
129
132
|
}
|
|
130
133
|
await store.set(keyOf(id), values);
|
|
134
|
+
|
|
131
135
|
return pick(values);
|
|
132
136
|
},
|
|
133
137
|
async update(id: string, input: Partial<Record<string, unknown>>) {
|
|
@@ -135,6 +139,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
135
139
|
if (!existing) throw new Error(`Not found: ${id}`);
|
|
136
140
|
const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
|
|
137
141
|
await store.set(keyOf(id), updated);
|
|
142
|
+
|
|
138
143
|
return pick(updated);
|
|
139
144
|
},
|
|
140
145
|
async delete(id: string) { return await store.delete(keyOf(id)); },
|
package/src/verify.ts
CHANGED
|
@@ -42,6 +42,7 @@ function registrationsOf(frond: FrondDescriptor): Map<string, Registration> {
|
|
|
42
42
|
put(storageKeyOf(e.name), 'storage');
|
|
43
43
|
put(repositoryKeyOf(e.name), 'repository');
|
|
44
44
|
}
|
|
45
|
+
|
|
45
46
|
return out;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -143,6 +144,7 @@ export function verify(app: { fronds: readonly FrondDescriptor[] }): Misplaced[]
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
}
|
|
147
|
+
|
|
146
148
|
return violations;
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { OperationContext } from './OperationContext.js';
|
|
2
2
|
import type { AppNext } from './AppNext.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A middleware answers what the chain answered — the same TYPE, whatever it does with the value.
|
|
6
|
+
*
|
|
7
|
+
* `T` is the whole rule and nothing enforces it at runtime: a middleware is handed a type it
|
|
8
|
+
* cannot name, so the only value of that type it can produce is the one `next()` gave it. It may
|
|
9
|
+
* observe it, log it, replace it with another of the same shape, or refuse by throwing; it may
|
|
10
|
+
* not wrap it in `{ data, meta }` nor invent one, because a caller reads the handler's signature
|
|
11
|
+
* and nothing tells that signature a middleware stood in the way.
|
|
12
|
+
*/
|
|
13
|
+
export type AppMiddleware = <T>(ctx: OperationContext, next: AppNext<T>) => Promise<T>;
|
|
5
14
|
|
|
6
15
|
// ── Runner ──────────────────────────────────────
|
|
7
16
|
|
|
@@ -18,7 +27,9 @@ export function runMiddlewares(
|
|
|
18
27
|
if (index < middlewares.length) {
|
|
19
28
|
return middlewares[index++](ctx, next);
|
|
20
29
|
}
|
|
30
|
+
|
|
21
31
|
return handler();
|
|
22
32
|
};
|
|
33
|
+
|
|
23
34
|
return next();
|
|
24
35
|
}
|
package/src/wire/AppNext.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** What a middleware holds: the rest of the chain, and the answer it will hand back. */
|
|
2
|
+
export type AppNext<T = unknown> = () => Promise<T>;
|
package/src/wire/Facade.ts
CHANGED
|
@@ -10,10 +10,22 @@ type Served = keyof FougereOperations & string;
|
|
|
10
10
|
*/
|
|
11
11
|
type AddressIn<Key> = Key extends `${infer Address}.${string}` ? Address : never;
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The facade built in front of a handler — the framework's second port, after `Storage`.
|
|
15
|
+
*
|
|
16
|
+
* What the handler knows is WHICH operations exist and what each one answers. The two ends are
|
|
17
|
+
* the port's own: an invocation goes in where the handler takes positional arguments, and a
|
|
18
|
+
* promise comes back where the handler may answer a bare value. A facade is a crossing, and a
|
|
19
|
+
* crossing is awaited before any transport — the dispatch resolves a route, runs the middlewares
|
|
20
|
+
* and awaits the collectors, so `readLocation(): string` was typed as answering now and never did.
|
|
21
|
+
*
|
|
22
|
+
* `Awaited` because a promise does not stack: `Promise.resolve(p)` IS `p`, so writing
|
|
23
|
+
* `Promise<R>` over an async handler would describe a `Promise<Promise<Post>>` that no value can
|
|
24
|
+
* have — `.then` would hand its callback a promise the runtime never delivers.
|
|
25
|
+
*/
|
|
14
26
|
export type Facade<T> = {
|
|
15
27
|
[K in keyof T]: T[K] extends (...args: never[]) => infer R
|
|
16
|
-
? (invocation?: InvocationContext) => R
|
|
28
|
+
? (invocation?: InvocationContext) => Promise<Awaited<R>>
|
|
17
29
|
: never;
|
|
18
30
|
};
|
|
19
31
|
|
package/src/wire/FougereError.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
|
|
|
20
20
|
|
|
21
21
|
constructor(options: FougereErrorOptions<Code>) {
|
|
22
22
|
super(options.message, { cause: options.cause });
|
|
23
|
-
this.name =
|
|
23
|
+
this.name = 'FougereError';
|
|
24
24
|
this.code = options.code;
|
|
25
25
|
this.entity = options.entity;
|
|
26
26
|
this.operation = options.operation;
|
|
@@ -41,6 +41,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
|
|
|
41
41
|
static fromJSON(json: unknown): FougereError {
|
|
42
42
|
const raw = (typeof json === 'object' && json !== null ? json : {}) as Record<string, unknown>;
|
|
43
43
|
const known = Object.values(ErrorCode).includes(raw.code as ErrorCode);
|
|
44
|
+
|
|
44
45
|
return new FougereError({
|
|
45
46
|
code: known ? (raw.code as ErrorCode) : ErrorCode.INTERNAL_ERROR,
|
|
46
47
|
message: typeof raw.message === 'string' ? raw.message : 'Unknown error',
|
|
@@ -65,5 +66,6 @@ export function validationErrorsOf(error: unknown): ValidationError[] | undefine
|
|
|
65
66
|
&& Array.isArray((entry as ValidationError).path)
|
|
66
67
|
&& typeof (entry as ValidationError).message === 'string',
|
|
67
68
|
);
|
|
69
|
+
|
|
68
70
|
return refusals.length === details.length ? refusals : undefined;
|
|
69
71
|
}
|
package/src/wire/Invocation.ts
CHANGED
|
@@ -8,6 +8,7 @@ function canonicalValue(value: unknown): unknown {
|
|
|
8
8
|
|
|
9
9
|
const prototype = Object.getPrototypeOf(value);
|
|
10
10
|
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
11
|
+
|
|
11
12
|
return canonicalRecord(value);
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -19,6 +20,7 @@ function canonicalRecord(value: unknown): Record<string, unknown> {
|
|
|
19
20
|
for (const [key, member] of Object.entries(value)) {
|
|
20
21
|
if (member !== undefined) result[key] = canonicalValue(member);
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
return Object.freeze(result);
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -36,9 +36,10 @@ export function cardinalityOf(type: TypeRef | undefined): OperationContract['car
|
|
|
36
36
|
if (!type) return undefined;
|
|
37
37
|
const inner = type.name === 'Promise' ? type.generics?.[0] : type;
|
|
38
38
|
if (!inner) return 'none';
|
|
39
|
-
if (inner.name === 'ListResult') return 'page';
|
|
39
|
+
if (inner.name === 'Page' || inner.name === 'ListResult') return 'page';
|
|
40
40
|
if (inner.array) return 'many';
|
|
41
41
|
if (PRIMITIVE_RETURNS.has(inner.name)) return 'none';
|
|
42
|
+
|
|
42
43
|
return inner.nullable || inner.undefined ? 'maybe' : 'one';
|
|
43
44
|
}
|
|
44
45
|
|
package/src/wire/Page.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a paged operation answers — the rows, and what the storage said about the rest of them.
|
|
3
|
+
*
|
|
4
|
+
* An ENVELOPE rather than the array `Storage.list` hands back: that one carries `total` and
|
|
5
|
+
* `hasMore` as properties OF the array, and `JSON.stringify` keeps only the indices. Measured
|
|
6
|
+
* 2026-09-18 — `page.total` is 42 in this process and `undefined` behind `fronds:`, off the same
|
|
7
|
+
* code. What crosses has to be named, so it is a field.
|
|
8
|
+
*
|
|
9
|
+
* `Storage.list` keeps its array: it answers inside a frond and never crosses.
|
|
10
|
+
*/
|
|
11
|
+
export interface Page<Row> {
|
|
12
|
+
items: Row[];
|
|
13
|
+
/** Only when the caller asked to count. */
|
|
14
|
+
total?: number;
|
|
15
|
+
hasMore?: boolean;
|
|
16
|
+
endCursor?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The array `Storage.list` answers, read as the page an operation hands over.
|
|
21
|
+
*
|
|
22
|
+
* A storage handed in by a host may answer the envelope already — `storageFactory` is its door,
|
|
23
|
+
* and nothing there is obliged to subclass an array. Taken as it comes in that case.
|
|
24
|
+
*/
|
|
25
|
+
export function pageOf<Row>(rows: Page<Row> | readonly Row[]): Page<Row> {
|
|
26
|
+
if (!Array.isArray(rows)) return rows as Page<Row>;
|
|
27
|
+
|
|
28
|
+
const { total, hasMore, endCursor } = rows as unknown as Omit<Page<Row>, 'items'>;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
items: [...rows],
|
|
32
|
+
...(total !== undefined && { total }),
|
|
33
|
+
...(hasMore !== undefined && { hasMore }),
|
|
34
|
+
...(endCursor !== undefined && { endCursor }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The value read as a page, or nothing.
|
|
40
|
+
*
|
|
41
|
+
* Read from the FORM, and arbitrated by the schema: an entity legally declaring a field named
|
|
42
|
+
* `items` answers a row, not a page, and it is the only thing that could tell the two apart.
|
|
43
|
+
*/
|
|
44
|
+
export function asPage(value: unknown, fields: Record<string, unknown>): Page<unknown> | undefined {
|
|
45
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
46
|
+
if ('items' in fields) return undefined;
|
|
47
|
+
const page = value as Page<unknown>;
|
|
48
|
+
|
|
49
|
+
return Array.isArray(page.items) ? page : undefined;
|
|
50
|
+
}
|
package/src/wire/RouteAddress.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { RouteAddressInput } from './RouteAddressInput.js';
|
|
|
3
3
|
/** Refuses an empty segment, so no address can be built that nothing can serve. */
|
|
4
4
|
function required(value: string, name: string): string {
|
|
5
5
|
if (value.trim().length === 0) throw new TypeError(`Route ${name} cannot be empty`);
|
|
6
|
+
|
|
6
7
|
return value;
|
|
7
8
|
}
|
|
8
9
|
|
|
@@ -38,6 +39,7 @@ export class RouteAddress {
|
|
|
38
39
|
/** Writes the address the way a log and an error message name it. */
|
|
39
40
|
toString(): string {
|
|
40
41
|
const audience = this.surface ? `${this.surface}/` : '';
|
|
42
|
+
|
|
41
43
|
return `${audience}${this.entity}.${this.operation}`;
|
|
42
44
|
}
|
|
43
45
|
|
package/src/wire/Signature.ts
CHANGED
|
@@ -10,4 +10,11 @@ export interface Signature {
|
|
|
10
10
|
inherited?: boolean;
|
|
11
11
|
/** The operation in words — the first sentence of the method's doc comment. */
|
|
12
12
|
description?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The first thing in the return type that is not data — its path, and the type sitting there.
|
|
15
|
+
*
|
|
16
|
+
* Written whenever a checker read the signature, and judged only where nothing converts the
|
|
17
|
+
* output: what a declared entity answers is encoded by its fields.
|
|
18
|
+
*/
|
|
19
|
+
notData?: string;
|
|
13
20
|
}
|
package/src/wire/binding.ts
CHANGED
|
@@ -38,6 +38,7 @@ const PRIMITIVES = new Set(['string', 'number', 'boolean']);
|
|
|
38
38
|
function coercionFor(typeName: string): 'number' | 'boolean' | undefined {
|
|
39
39
|
if (typeName === 'number') return 'number';
|
|
40
40
|
if (typeName === 'boolean') return 'boolean';
|
|
41
|
+
|
|
41
42
|
return undefined;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -27,6 +27,7 @@ export function assertIdentityCard(value: unknown, source: string): IdentityCard
|
|
|
27
27
|
if (!frond || typeof frond.name !== 'string') throw cardRefusal(source, 'a frond with no name');
|
|
28
28
|
if (!Array.isArray(frond.facades)) throw cardRefusal(source, `frond '${frond.name}' has no valid facades array`);
|
|
29
29
|
}
|
|
30
|
+
|
|
30
31
|
return card as IdentityCard;
|
|
31
32
|
}
|
|
32
33
|
|
package/src/wire/drift.ts
CHANGED
|
@@ -29,6 +29,7 @@ function facadesOf(card: IdentityCard, frond: string): Map<string, Answering> {
|
|
|
29
29
|
found.set(facade.name, { ops: new Set(facade.ops.map((op) => op.name)), schema: facade.schema });
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
|
|
32
33
|
return found;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -38,6 +39,7 @@ function factsOf(card: IdentityCard, frond: string): Map<string, SchemaDescripto
|
|
|
38
39
|
if (one.name !== frond) continue;
|
|
39
40
|
for (const fact of one.facts ?? []) found.set(fact.name, fact.schema as SchemaDescriptor | undefined);
|
|
40
41
|
}
|
|
42
|
+
|
|
41
43
|
return found;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -117,6 +119,7 @@ export function explain(drift: CardDrift): string[] {
|
|
|
117
119
|
if (changes === 'gone') { lines.push(`${fact} — you subscribe to it, it is no longer announced`); continue; }
|
|
118
120
|
for (const change of changes) lines.push(`${fact} — ${describe(change)} → re-sync and deploy the readers, THEN the sender`);
|
|
119
121
|
}
|
|
122
|
+
|
|
120
123
|
return lines;
|
|
121
124
|
}
|
|
122
125
|
|
package/src/wire/http-error.ts
CHANGED
|
@@ -40,6 +40,7 @@ export function toPublicError(err: FougereError): ReturnType<FougereError['toJSO
|
|
|
40
40
|
if (err.code !== ErrorCode.INTERNAL_ERROR) return err.toJSON();
|
|
41
41
|
const where = [err.entity, err.operation].filter(Boolean).join('.');
|
|
42
42
|
log.error(`${where || 'internal'}: ${err.message}`, err.cause ?? err);
|
|
43
|
+
|
|
43
44
|
return {
|
|
44
45
|
code: ErrorCode.INTERNAL_ERROR,
|
|
45
46
|
message: 'Internal error',
|
|
@@ -60,5 +61,6 @@ export function toHttpError(err: unknown): { status: number; body: ReturnType<Fo
|
|
|
60
61
|
message: (err as { message?: string })?.message ?? 'Internal error',
|
|
61
62
|
cause: err,
|
|
62
63
|
});
|
|
64
|
+
|
|
63
65
|
return { status: 500, body: toPublicError(framed) };
|
|
64
66
|
}
|
|
@@ -19,6 +19,7 @@ export function loggerMiddleware(logger: Logger): AppMiddleware {
|
|
|
19
19
|
const result = await next();
|
|
20
20
|
const ms = (performance.now() - start).toFixed(1);
|
|
21
21
|
logger.info(`${ctx.entity}.${ctx.operation} (${ms}ms)`);
|
|
22
|
+
|
|
22
23
|
return result;
|
|
23
24
|
} catch (err) {
|
|
24
25
|
const ms = (performance.now() - start).toFixed(1);
|