@fougere/core 0.12.1-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 +2 -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 +3 -1
- 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.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.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/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/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.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 +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/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.map +1 -1
- package/dist/wire/OperationContract.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 +19 -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 +4 -1
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +39 -2
- package/src/boot/remote.ts +1 -0
- 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 +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 +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/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/loader.ts +1 -0
- package/src/prefab/CrudConstructor.ts +1 -0
- 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 +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +1 -0
- 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
|
@@ -22,6 +22,7 @@ export class LocalRoutePolicy implements RoutePolicy {
|
|
|
22
22
|
const hosted = this.hostedNames(call.address.surface);
|
|
23
23
|
const entity = call.address.entity;
|
|
24
24
|
const surface = call.address.surface;
|
|
25
|
+
|
|
25
26
|
return new FougereError({
|
|
26
27
|
code: ErrorCode.NOT_FOUND,
|
|
27
28
|
message: (surface
|
|
@@ -35,6 +35,7 @@ export class RouteRegistry {
|
|
|
35
35
|
entity: address.entity,
|
|
36
36
|
operation: address.operation,
|
|
37
37
|
}).key());
|
|
38
|
+
|
|
38
39
|
return shared?.kind === 'system' ? shared : undefined;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -48,6 +49,7 @@ export class RouteRegistry {
|
|
|
48
49
|
|
|
49
50
|
const resolution = this.resolveUnknown(address).finally(() => this.pending.delete(key));
|
|
50
51
|
this.pending.set(key, resolution);
|
|
52
|
+
|
|
51
53
|
return resolution;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -56,8 +58,10 @@ export class RouteRegistry {
|
|
|
56
58
|
const route = await resolver(address);
|
|
57
59
|
if (!route) continue;
|
|
58
60
|
this.register(route);
|
|
61
|
+
|
|
59
62
|
return route;
|
|
60
63
|
}
|
|
64
|
+
|
|
61
65
|
return undefined;
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FieldSet, FieldValueValidator, InputRefusal, type Fields, type Verdict } from '@fougere/schema';
|
|
1
|
+
import { FieldSet, FieldValueValidator, InputRefusal, Role, type Fields, type Verdict } from '@fougere/schema';
|
|
2
2
|
import { COMPARISONS, comparisonOf, unknownIn } from '../storage/Comparison.js';
|
|
3
3
|
import { assertListOptions } from '../storage/Storage.js';
|
|
4
4
|
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
@@ -69,12 +69,14 @@ export class StorageGuard {
|
|
|
69
69
|
guarded.create = async function (...args) {
|
|
70
70
|
args[0] = validation.validated(args[0], 'create');
|
|
71
71
|
await validation.targetsOf([args[0]], 'create');
|
|
72
|
+
|
|
72
73
|
return writer.create.apply(this, args);
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
guarded.update = async function (...args) {
|
|
76
77
|
args[1] = validation.validated(args[1], 'update');
|
|
77
78
|
await validation.targetsOf([args[1]], 'update');
|
|
79
|
+
|
|
78
80
|
return writer.update.apply(this, args);
|
|
79
81
|
};
|
|
80
82
|
|
|
@@ -83,6 +85,7 @@ export class StorageGuard {
|
|
|
83
85
|
guarded.upsert = async function (...args) {
|
|
84
86
|
args[0] = validation.validated(args[0], 'upsert');
|
|
85
87
|
await validation.targetsOf([args[0]], 'upsert');
|
|
88
|
+
|
|
86
89
|
return upsert.apply(this, args);
|
|
87
90
|
};
|
|
88
91
|
}
|
|
@@ -94,6 +97,7 @@ export class StorageGuard {
|
|
|
94
97
|
guarded.upsertAll = async function (...args) {
|
|
95
98
|
args[0] = args[0].map((row, index) => validation.validated(row, 'upsertAll', index));
|
|
96
99
|
await validation.targetsOf(args[0], 'upsertAll');
|
|
100
|
+
|
|
97
101
|
return upsertAll.apply(this, args);
|
|
98
102
|
};
|
|
99
103
|
}
|
|
@@ -122,6 +126,7 @@ export class StorageGuard {
|
|
|
122
126
|
private rowAt(row: unknown, index: number): string {
|
|
123
127
|
const primary = FieldSet.of(this.fields).primary;
|
|
124
128
|
const key = primary === undefined ? undefined : (row as Record<string, unknown>)[primary];
|
|
129
|
+
|
|
125
130
|
return key === undefined ? `row ${index} of this page` : `row ${primary} ${JSON.stringify(key)}`;
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -213,6 +218,8 @@ export class StorageGuard {
|
|
|
213
218
|
private written(key: string, item: unknown): Verdict {
|
|
214
219
|
const field = this.fields[key];
|
|
215
220
|
if (!field) return { message: InputRefusal.unknownField };
|
|
221
|
+
// Declared, and still nothing to write: the other side of the relation carries the key.
|
|
222
|
+
if (Role.of(field).isCollection()) return { message: InputRefusal.readOnly };
|
|
216
223
|
if (item === undefined) return { value: item };
|
|
217
224
|
|
|
218
225
|
return FieldValueValidator.of(field).parse(item);
|
|
@@ -220,6 +227,7 @@ export class StorageGuard {
|
|
|
220
227
|
|
|
221
228
|
private value(field: Fields[string], asked: unknown): Verdict {
|
|
222
229
|
if (asked === null || asked === undefined) return { value: asked };
|
|
230
|
+
|
|
223
231
|
return FieldValueValidator.of(field).parse(asked);
|
|
224
232
|
}
|
|
225
233
|
|
package/src/identity-keys.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { b64url } from './crypto/encoding.js';
|
|
|
5
5
|
/** A fresh Ed25519 pair, PEM both ways. The root's and a frond's are the same kind. */
|
|
6
6
|
export function generateKeyPair(): { privateKey: string; publicKey: string } {
|
|
7
7
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
8
|
+
|
|
8
9
|
return {
|
|
9
10
|
privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
10
11
|
publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
@@ -26,5 +27,6 @@ export function issueGrant(
|
|
|
26
27
|
const header = b64url(JSON.stringify({ alg: 'EdDSA', typ: 'fougere-grant' }));
|
|
27
28
|
const payload = b64url(JSON.stringify({ sub: name, jwk, iat: now, exp: now + ttlDays * 86_400_000 }));
|
|
28
29
|
const signingInput = `${header}.${payload}`;
|
|
30
|
+
|
|
29
31
|
return `${signingInput}.${b64url(new Uint8Array(sign(null, Buffer.from(signingInput), createPrivateKey(rootPrivateKey))))}`;
|
|
30
32
|
}
|
package/src/loader.ts
CHANGED
|
@@ -11,6 +11,7 @@ const defaultLoader: ModuleLoader = async (filePath, options) => {
|
|
|
11
11
|
// On the URL, never on the path: `pathToFileURL` percent-encodes a `?` into the
|
|
12
12
|
// filename, and the import then looks for a file whose name ends in `%3Fv=…`.
|
|
13
13
|
const url = pathToFileURL(filePath).href;
|
|
14
|
+
|
|
14
15
|
return await import(options?.fresh ? `${url}?v=${Date.now()}` : url);
|
|
15
16
|
};
|
|
16
17
|
|
|
@@ -52,6 +52,7 @@ const returns = (raw: string, name: string, extra?: { array?: boolean; nullable?
|
|
|
52
52
|
function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<string, OperationContract> {
|
|
53
53
|
const name = (entity as { name?: string }).name ?? 'Entity';
|
|
54
54
|
const input = inputParam(name);
|
|
55
|
+
|
|
55
56
|
return {
|
|
56
57
|
list: {
|
|
57
58
|
output: entity, cardinality: 'page',
|
package/src/prefab/collector.ts
CHANGED
package/src/prefab/presenter.ts
CHANGED
package/src/storage/Storage.ts
CHANGED
|
@@ -159,6 +159,7 @@ const HELD = 'Storage';
|
|
|
159
159
|
*/
|
|
160
160
|
export function togetherKeyOf(entities: readonly string[], providers: readonly string[] = []): string {
|
|
161
161
|
const named = entities.map(upperFirst).join(SEPARATOR);
|
|
162
|
+
|
|
162
163
|
return `${named}${providers.length ? KINDS + providers.map(upperFirst).join(SEPARATOR) : ''}${FRAME}`;
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -174,5 +175,6 @@ export function membersOfTogetherKey(key: string): { entities: string[]; provide
|
|
|
174
175
|
if (key.length <= FRAME.length || !key.endsWith(FRAME)) return undefined;
|
|
175
176
|
const [entities = '', providers = ''] = key.slice(0, -FRAME.length).split(KINDS);
|
|
176
177
|
const split = (list: string) => list.split(SEPARATOR).filter(Boolean);
|
|
178
|
+
|
|
177
179
|
return { entities: split(entities), providers: split(providers) };
|
|
178
180
|
}
|
package/src/storage/Store.ts
CHANGED
|
@@ -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.
|
|
@@ -57,6 +57,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
57
57
|
const previous = await store.get(keyOf(id));
|
|
58
58
|
const values = previous ? { ...previous, ...applyUpdate(fields, input) } : created;
|
|
59
59
|
await store.set(keyOf(id), values);
|
|
60
|
+
|
|
60
61
|
return pick(values);
|
|
61
62
|
};
|
|
62
63
|
|
|
@@ -71,10 +72,12 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
71
72
|
},
|
|
72
73
|
async findById(id: string) {
|
|
73
74
|
const values = await store.get(keyOf(id));
|
|
75
|
+
|
|
74
76
|
return values && pick(values);
|
|
75
77
|
},
|
|
76
78
|
async findBy(criteria: Record<string, unknown>) {
|
|
77
79
|
const values = (await store.all()).find((held) => matches(held, criteria));
|
|
80
|
+
|
|
78
81
|
return values && pick(values);
|
|
79
82
|
},
|
|
80
83
|
async findAllBy(criteria: Record<string, unknown>) {
|
|
@@ -87,6 +90,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
87
90
|
const values = await store.get(keyOf(id));
|
|
88
91
|
if (values) found.set(String(id), pick(values));
|
|
89
92
|
}
|
|
93
|
+
|
|
90
94
|
return found;
|
|
91
95
|
},
|
|
92
96
|
// The dual, same contract as SQL: grouped by the value read off the instance.
|
|
@@ -100,11 +104,13 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
100
104
|
const bucket = grouped.get(key);
|
|
101
105
|
if (bucket) bucket.push(pick(values)); else grouped.set(key, [pick(values)]);
|
|
102
106
|
}
|
|
107
|
+
|
|
103
108
|
return grouped;
|
|
104
109
|
},
|
|
105
110
|
upsert,
|
|
106
111
|
async upsertAll(inputs: readonly Partial<Record<string, unknown>>[]) {
|
|
107
112
|
for (const input of inputs) await upsert(input);
|
|
113
|
+
|
|
108
114
|
return inputs.length;
|
|
109
115
|
},
|
|
110
116
|
async create(input: Partial<Record<string, unknown>>) {
|
|
@@ -125,6 +131,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
125
131
|
throw new Error(`${name}.create: '${pk}' ${JSON.stringify(id)} already exists.`);
|
|
126
132
|
}
|
|
127
133
|
await store.set(keyOf(id), values);
|
|
134
|
+
|
|
128
135
|
return pick(values);
|
|
129
136
|
},
|
|
130
137
|
async update(id: string, input: Partial<Record<string, unknown>>) {
|
|
@@ -132,6 +139,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
132
139
|
if (!existing) throw new Error(`Not found: ${id}`);
|
|
133
140
|
const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
|
|
134
141
|
await store.set(keyOf(id), updated);
|
|
142
|
+
|
|
135
143
|
return pick(updated);
|
|
136
144
|
},
|
|
137
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
|
|
package/src/wire/FougereError.ts
CHANGED
|
@@ -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
|
|
|
@@ -39,6 +39,7 @@ export function cardinalityOf(type: TypeRef | undefined): OperationContract['car
|
|
|
39
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/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/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);
|