@fougere/core 0.12.1-alpha.0 → 0.12.4-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts +1 -4
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +182 -173
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +5 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +11 -13
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +1 -5
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/Source.js +3 -3
- package/dist/Source.js.map +1 -1
- package/dist/SourceView.d.ts +0 -2
- package/dist/SourceView.d.ts.map +1 -1
- package/dist/boot/App.d.ts +2 -4
- package/dist/boot/App.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +5 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +19 -10
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/AuthRuntime.d.ts +5 -12
- package/dist/boot/AuthRuntime.d.ts.map +1 -1
- package/dist/boot/AuthRuntime.js +2 -1
- package/dist/boot/AuthRuntime.js.map +1 -1
- package/dist/boot/CreateAppOptions.d.ts +0 -11
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +3 -6
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/Hosting.d.ts +2 -1
- package/dist/boot/Hosting.d.ts.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +143 -265
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +105 -115
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts +5 -0
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js +8 -0
- package/dist/descriptor/Fronds.js.map +1 -1
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -0
- package/dist/descriptor/index.js.map +1 -1
- package/dist/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts +3 -2
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js +11 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Comparison.d.ts +2 -2
- package/dist/storage/Comparison.d.ts.map +1 -1
- package/dist/storage/Comparison.js +3 -1
- package/dist/storage/Comparison.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +1 -1
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/Emit.d.ts +0 -2
- package/dist/wire/Emit.d.ts.map +1 -1
- package/dist/wire/Emit.js +1 -1
- package/dist/wire/Emit.js.map +1 -1
- package/dist/wire/Facade.d.ts +7 -0
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js +13 -0
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts +0 -4
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/OperationsMap.d.ts +0 -3
- package/dist/wire/OperationsMap.d.ts.map +1 -1
- package/dist/wire/OperationsMap.js +0 -5
- package/dist/wire/OperationsMap.js.map +1 -1
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +236 -198
- package/src/FougereConfig.ts +17 -16
- package/src/FrondConfig.ts +2 -0
- package/src/FrondDeclaration.ts +1 -7
- package/src/Source.ts +3 -3
- package/src/SourceView.ts +0 -2
- package/src/boot/App.ts +2 -4
- package/src/boot/AppLifecycle.ts +22 -10
- package/src/boot/AuthRuntime.ts +6 -13
- package/src/boot/CreateAppOptions.ts +0 -11
- package/src/boot/Emissions.ts +7 -7
- package/src/boot/Hosting.ts +2 -1
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +175 -300
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +123 -146
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +42 -10
- package/src/boot/remote.ts +1 -0
- package/src/boot/seed.ts +3 -5
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +1 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +12 -0
- package/src/descriptor/index.ts +1 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +18 -2
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +2 -4
- package/src/loader.ts +1 -0
- package/src/node.ts +1 -1
- package/src/prefab/CrudConstructor.ts +1 -11
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Comparison.ts +7 -4
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +9 -1
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +2 -0
- package/src/wire/Emit.ts +1 -1
- package/src/wire/Facade.ts +17 -10
- package/src/wire/FougereError.ts +2 -0
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/OperationsMap.ts +0 -10
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
- package/dist/boot/AuthConfig.d.ts +0 -14
- package/dist/boot/AuthConfig.d.ts.map +0 -1
- package/dist/boot/AuthConfig.js +0 -2
- package/dist/boot/AuthConfig.js.map +0 -1
- package/dist/boot/AuthContext.d.ts +0 -12
- package/dist/boot/AuthContext.d.ts.map +0 -1
- package/dist/boot/AuthContext.js +0 -2
- package/dist/boot/AuthContext.js.map +0 -1
- package/dist/storage/ComparisonName.d.ts +0 -3
- package/dist/storage/ComparisonName.d.ts.map +0 -1
- package/dist/storage/ComparisonName.js +0 -2
- package/dist/storage/ComparisonName.js.map +0 -1
- package/src/boot/AuthConfig.ts +0 -14
- package/src/boot/AuthContext.ts +0 -12
- package/src/storage/ComparisonName.ts +0 -3
package/src/builtin/config.ts
CHANGED
package/src/contract.ts
CHANGED
package/src/crypto/encoding.ts
CHANGED
|
@@ -13,6 +13,7 @@ export function b64url(input: Uint8Array | string): string {
|
|
|
13
13
|
// One character at a time: `String.fromCharCode(...bytes)` overflows the argument
|
|
14
14
|
// limit on a key, and a signature is small enough that the loop costs nothing.
|
|
15
15
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
16
|
+
|
|
16
17
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export function unb64url(input: string): Uint8Array {
|
|
|
20
21
|
const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/'));
|
|
21
22
|
const bytes = new Uint8Array(binary.length);
|
|
22
23
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
|
|
23
25
|
return bytes;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -28,6 +30,7 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
28
30
|
const binary = atob(input);
|
|
29
31
|
const bytes = new Uint8Array(binary.length);
|
|
30
32
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
|
|
31
34
|
return bytes;
|
|
32
35
|
};
|
|
33
36
|
|
|
@@ -35,5 +38,6 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
35
38
|
export function derOf(pem: string): Uint8Array {
|
|
36
39
|
const body = pem.replace(/-----(BEGIN|END)[^-]+-----/g, '').replace(/\s+/g, '');
|
|
37
40
|
if (body.length === 0) throw new Error('Not a PEM: no base64 body between the armour');
|
|
41
|
+
|
|
38
42
|
return unb64(body);
|
|
39
43
|
}
|
package/src/crypto/node.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const crypto: CryptoPort = {
|
|
|
13
13
|
async signerOf(privateKeyPem): Promise<Signer> {
|
|
14
14
|
// Parsed here and captured, so a call signs against a KeyObject and never a string.
|
|
15
15
|
const key = createPrivateKey(privateKeyPem);
|
|
16
|
+
|
|
16
17
|
return { async sign(data) { return new Uint8Array(sign(null, data, key)); } };
|
|
17
18
|
},
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export const crypto: CryptoPort = {
|
|
|
20
21
|
const parsed = typeof key === 'string'
|
|
21
22
|
? createPublicKey(key)
|
|
22
23
|
: createPublicKey({ key: key as never, format: 'jwk' });
|
|
24
|
+
|
|
23
25
|
return { async verify(data, signature) { return verify(null, data, parsed, signature); } };
|
|
24
26
|
},
|
|
25
27
|
};
|
package/src/crypto/webcrypto.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const crypto: CryptoPort = {
|
|
|
33
33
|
// Imported once, at boot. Doing it per call would put an async key parse inside
|
|
34
34
|
// every outgoing request — the reason the port hands back a signer at all.
|
|
35
35
|
const key = await subtle().importKey('pkcs8', derOf(privateKeyPem), ED25519, false, ['sign']);
|
|
36
|
+
|
|
36
37
|
return { async sign(data) { return new Uint8Array(await subtle().sign(ED25519, key, data)); } };
|
|
37
38
|
},
|
|
38
39
|
|
|
@@ -40,6 +41,7 @@ export const crypto: CryptoPort = {
|
|
|
40
41
|
const imported = typeof key === 'string'
|
|
41
42
|
? await subtle().importKey('spki', derOf(key), ED25519, false, ['verify'])
|
|
42
43
|
: await subtle().importKey('jwk', key, ED25519, false, ['verify']);
|
|
44
|
+
|
|
43
45
|
return { verify: (data, signature) => subtle().verify(ED25519, imported, signature, data) };
|
|
44
46
|
},
|
|
45
47
|
};
|
package/src/descriptor/Fronds.ts
CHANGED
|
@@ -12,6 +12,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
12
12
|
static hosting(fronds: readonly FrondDescriptor[]): Fronds {
|
|
13
13
|
const all = new Fronds();
|
|
14
14
|
all.push(...fronds);
|
|
15
|
+
|
|
15
16
|
return all;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -26,6 +27,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
26
27
|
const found = frond.entities.find((e) => e.name === name);
|
|
27
28
|
if (found) return found;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
return undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -39,6 +41,16 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
39
41
|
return this.flatMap((frond) => frond.entities.map((e) => e.name)).sort();
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether a named surface serves an entity, as its owning frond declared it — `undefined` when
|
|
46
|
+
* that frond declares no such surface, and a facade registered there answers as it is.
|
|
47
|
+
*/
|
|
48
|
+
admits(surface: string, entity: string): boolean | undefined {
|
|
49
|
+
const declared = this.owner(entity)?.surfaces?.[surface];
|
|
50
|
+
|
|
51
|
+
return declared?.some((name) => name.toLowerCase() === entity.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/** The names a call may address, sorted — what a NOT_FOUND must print. */
|
|
43
55
|
servedNames(surface?: string): string[] {
|
|
44
56
|
return this.flatMap((frond) => frond.handlers
|
package/src/descriptor/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { type TypeRef } from '../wire/TypeRef.js';
|
|
|
23
23
|
export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
|
|
24
24
|
export type { Signature } from '../wire/Signature.js';
|
|
25
25
|
export { awaitKeyOf, emitKeyOf } from '../wire/Emit.js';
|
|
26
|
+
export { addressOf } from '../wire/Facade.js';
|
|
26
27
|
export { storageKeyOf } from '../storage/Storage.js';
|
|
27
28
|
export { getPresenterFields } from '../prefab/presenter.js';
|
|
28
29
|
export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
|
|
@@ -71,6 +71,7 @@ export class Dispatcher implements DispatchPort {
|
|
|
71
71
|
this.lifecycle.publish(DispatchEvent.resolved(call, route.kind));
|
|
72
72
|
const result = await route.execute(call);
|
|
73
73
|
this.lifecycle.publish(DispatchEvent.completed(call, route.kind));
|
|
74
|
+
|
|
74
75
|
return result;
|
|
75
76
|
} catch (error) {
|
|
76
77
|
this.lifecycle.publish(DispatchEvent.failed(call, error, route?.kind));
|
|
@@ -116,15 +116,23 @@ export class HandlerFacade {
|
|
|
116
116
|
|
|
117
117
|
const args = contract.binding ? await this.arguments.resolve(contract.binding, validated) : [];
|
|
118
118
|
const { instance, method } = this.resolveImplementation(op);
|
|
119
|
-
const
|
|
120
|
-
const output = view.project(await instance[method](...args));
|
|
119
|
+
const answered = await instance[method](...args);
|
|
121
120
|
|
|
121
|
+
const view = this.viewOf(op);
|
|
122
122
|
const { presenter } = this.facade;
|
|
123
|
+
const computed = view.closed || !presenter
|
|
124
|
+
? answered
|
|
125
|
+
: await this.present(op, presenter, answered, validated);
|
|
123
126
|
|
|
124
|
-
return view.
|
|
127
|
+
return view.project(computed);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The computed fields a presenter adds, over the rows the handler answered and BEFORE the
|
|
132
|
+
* view projects them. A presenter runs on the server, so what it reads is the row the
|
|
133
|
+
* storage holds: a `writeOnly` field is gone by the time the client sees it, and deriving
|
|
134
|
+
* from one is the whole reason to declare it.
|
|
135
|
+
*/
|
|
128
136
|
private present(
|
|
129
137
|
op: string,
|
|
130
138
|
presenter: PresenterEntry,
|
|
@@ -180,12 +188,14 @@ export class HandlerFacade {
|
|
|
180
188
|
operation?.outputClosed ?? false,
|
|
181
189
|
);
|
|
182
190
|
this.cachedViews.set(op, resolved);
|
|
191
|
+
|
|
183
192
|
return resolved;
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
/** The handler itself, resolved on first call — never at boot. */
|
|
187
196
|
private resolveHandler(): any {
|
|
188
197
|
if (!this.instance) this.instance = this.scope.resolve(this.handlerKey);
|
|
198
|
+
|
|
189
199
|
return this.instance;
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -208,6 +218,7 @@ export class HandlerFacade {
|
|
|
208
218
|
+ `${operation.implementation.method}, but boot found ${matches.length} matching handlers.`,
|
|
209
219
|
);
|
|
210
220
|
}
|
|
221
|
+
|
|
211
222
|
return matches[0]!;
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -225,6 +236,7 @@ export class HandlerFacade {
|
|
|
225
236
|
instance = this.scope.resolve(key);
|
|
226
237
|
this.implementationInstances.set(key, instance);
|
|
227
238
|
}
|
|
239
|
+
|
|
228
240
|
return { instance, method: operation.implementation.method };
|
|
229
241
|
}
|
|
230
242
|
}
|
package/src/dispatch/InFlight.ts
CHANGED
|
@@ -23,6 +23,7 @@ export class InFlight {
|
|
|
23
23
|
|
|
24
24
|
this.running++;
|
|
25
25
|
let released = false;
|
|
26
|
+
|
|
26
27
|
return () => {
|
|
27
28
|
if (released) return;
|
|
28
29
|
released = true;
|
|
@@ -35,12 +36,27 @@ export class InFlight {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/** Close the door, and answer once the calls already running are done — or refuse at the deadline, naming what is left. */
|
|
40
|
+
async drain(timeoutMs?: number): Promise<void> {
|
|
39
41
|
this.accepting = false;
|
|
42
|
+
if (timeoutMs === undefined) return this.whenIdle();
|
|
43
|
+
|
|
44
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
45
|
+
|
|
46
|
+
await Promise.race([
|
|
47
|
+
this.whenIdle().then(() => clearTimeout(timer)),
|
|
48
|
+
new Promise<never>((_, reject) => {
|
|
49
|
+
timer = setTimeout(
|
|
50
|
+
() => reject(new Error(`[drain] ${this.running} call(s) still running after ${timeoutMs}ms`)),
|
|
51
|
+
timeoutMs,
|
|
52
|
+
);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
whenIdle(): Promise<void> {
|
|
57
|
+
private whenIdle(): Promise<void> {
|
|
43
58
|
if (this.running === 0) return Promise.resolve();
|
|
59
|
+
|
|
44
60
|
return new Promise((resolve) => this.idle.push(resolve));
|
|
45
61
|
}
|
|
46
62
|
}
|
|
@@ -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/index.ts
CHANGED
|
@@ -91,7 +91,7 @@ export type { FougereOperations } from './wire/FougereOperations.js';
|
|
|
91
91
|
export type { HandlerOf } from './wire/HandlerOf.js';
|
|
92
92
|
export type { Refused } from './wire/Refused.js';
|
|
93
93
|
export type { Rows } from './wire/Rows.js';
|
|
94
|
-
export type
|
|
94
|
+
export { addressOf, type Facade } from './wire/Facade.js';
|
|
95
95
|
export type {
|
|
96
96
|
FrondCall,
|
|
97
97
|
Transport,
|
|
@@ -150,9 +150,7 @@ export type { PresenterEntry } from './descriptor/PresenterEntry.js';
|
|
|
150
150
|
export type { PresenterFieldMeta } from './descriptor/PresenterFieldMeta.js';
|
|
151
151
|
export type { ProviderEntry } from './descriptor/ProviderEntry.js';
|
|
152
152
|
export type { SeedEntry } from './descriptor/SeedEntry.js';
|
|
153
|
-
export type
|
|
154
|
-
export type { AuthContext } from './boot/AuthContext.js';
|
|
155
|
-
export type { AuthRuntime } from './boot/AuthRuntime.js';
|
|
153
|
+
export { AUTH, type AuthRuntime } from './boot/AuthRuntime.js';
|
|
156
154
|
export { Fronds } from './descriptor/Fronds.js';
|
|
157
155
|
export { verify, type Misplaced } from './verify.js';
|
|
158
156
|
// Same question as verify(), answered from the source text instead of the model.
|
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
|
|
package/src/node.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** What Fougere does with a filesystem, minus the scan — that is `@fougere/compiler`. */
|
|
2
2
|
export { setModuleLoader, getModuleLoader } from './loader.js';
|
|
3
|
-
export { loadConfig, loadCascadedConfig, remotesOf } from './FougereConfig.js';
|
|
3
|
+
export { configFileIn, loadConfig, loadCascadedConfig, remotesOf } from './FougereConfig.js';
|
|
4
4
|
export { statedModules } from './StatedModules.js';
|
|
5
5
|
export { defineFrond, loadFrondConfig } from './FrondConfig.js';
|
|
6
6
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { lowerFirst, type EntityConstructor, type SchemaView } from '@fougere/schema';
|
|
2
2
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
3
|
-
import type { ListResult } from '../storage/ListResult.js';
|
|
4
3
|
import type { Storage } from '../storage/Storage.js';
|
|
5
4
|
import type { OperationContract } from '../wire/OperationContract.js';
|
|
6
5
|
import { targetOf } from './prefab.js';
|
|
7
|
-
import type { CrudOpName } from './CrudOpName.js';
|
|
8
6
|
import type { CrudViews } from './CrudViews.js';
|
|
9
7
|
import { pageOf, type Page } from '../wire/Page.js';
|
|
10
8
|
import type { CrudOps } from './CrudOps.js';
|
|
@@ -52,6 +50,7 @@ const returns = (raw: string, name: string, extra?: { array?: boolean; nullable?
|
|
|
52
50
|
function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<string, OperationContract> {
|
|
53
51
|
const name = (entity as { name?: string }).name ?? 'Entity';
|
|
54
52
|
const input = inputParam(name);
|
|
53
|
+
|
|
55
54
|
return {
|
|
56
55
|
list: {
|
|
57
56
|
output: entity, cardinality: 'page',
|
|
@@ -87,15 +86,6 @@ function asCrudConstructor<T, V>(impl: object): CrudConstructor<T, V> {
|
|
|
87
86
|
return impl as CrudConstructor<T, V>;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
/** The view an op emits, fabricated. */
|
|
91
|
-
type OutOf<V, K extends CrudOpName, T> =
|
|
92
|
-
// Bracketed on purpose: a naked `V extends …` DISTRIBUTES, and the no-view default
|
|
93
|
-
// is the empty map, whose `keyof` is `never` — distribution would then collapse
|
|
94
|
-
// every op's output to `never` instead of falling through to the entity.
|
|
95
|
-
[V] extends [EntityConstructor] ? InstanceType<V & EntityConstructor>
|
|
96
|
-
: K extends keyof V ? (V[K] extends EntityConstructor ? InstanceType<V[K]> : T)
|
|
97
|
-
: T;
|
|
98
|
-
|
|
99
89
|
/** The prefab handler class — its ops, plus the statics the bootstrap and adapters read. */
|
|
100
90
|
export interface CrudConstructor<T, V = {}> {
|
|
101
91
|
// `Storage<T>`, not the bare `Storage`: a handler that injects a second storage has to
|
package/src/prefab/collector.ts
CHANGED
package/src/prefab/presenter.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type Field, Shapes } from '@fougere/schema';
|
|
2
|
-
import type { ComparisonName } from './ComparisonName.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* The comparisons a criterion may name.
|
|
@@ -21,7 +20,11 @@ export interface Comparison {
|
|
|
21
20
|
isNull?: boolean;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
export
|
|
23
|
+
export type ComparisonName = keyof Comparison;
|
|
24
|
+
|
|
25
|
+
export const COMPARISONS = Object.keys({
|
|
26
|
+
gte: true, lte: true, gt: true, lt: true, ne: true, between: true, contains: true, notIn: true, isNull: true,
|
|
27
|
+
} satisfies Record<ComparisonName, true>);
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Is this criterion a comparison, or a value that happens to be an object?
|
|
@@ -42,11 +45,11 @@ export function comparisonOf(field: Field | undefined, asked: unknown): Comparis
|
|
|
42
45
|
/** The comparisons a criterion names, in the order the caller wrote them. */
|
|
43
46
|
export function comparisonsIn(comparison: Comparison): [ComparisonName, unknown][] {
|
|
44
47
|
return Object.entries(comparison).filter(
|
|
45
|
-
(entry): entry is [ComparisonName, unknown] =>
|
|
48
|
+
(entry): entry is [ComparisonName, unknown] => COMPARISONS.includes(entry[0]),
|
|
46
49
|
);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/** What a comparison names that this vocabulary does not — a typo, said as one. */
|
|
50
53
|
export function unknownIn(comparison: Comparison): string[] {
|
|
51
|
-
return Object.keys(comparison).filter((name) => !
|
|
54
|
+
return Object.keys(comparison).filter((name) => !COMPARISONS.includes(name));
|
|
52
55
|
}
|
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/Emit.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function factOfEmitKey(key: string): string | undefined {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** The fact behind an awaited emission's key — the dual, read the same way. */
|
|
38
|
-
|
|
38
|
+
function factOfAwaitKey(key: string): string | undefined {
|
|
39
39
|
return behind(key, AWAIT_SUFFIX);
|
|
40
40
|
}
|
|
41
41
|
|
package/src/wire/Facade.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { FougereOperations } from './FougereOperations.js';
|
|
3
|
-
|
|
4
|
-
type Served = keyof FougereOperations & string;
|
|
1
|
+
import { lowerFirst } from '@fougere/schema';
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
* Both halves below take the union through a type PARAMETER, which is what makes the
|
|
8
|
-
* conditional distribute: `Served` is an alias, and an alias is matched whole — `'a.x' | 'b.y'`
|
|
9
|
-
* does not extend `` `${string}.${string}` `` as one thing, so the naive form answers `never`.
|
|
10
|
-
*/
|
|
11
|
-
type AddressIn<Key> = Key extends `${infer Address}.${string}` ? Address : never;
|
|
3
|
+
import type { InvocationContext } from './InvocationContext.js';
|
|
12
4
|
|
|
13
5
|
/**
|
|
14
6
|
* The facade built in front of a handler — the framework's second port, after `Storage`.
|
|
@@ -34,6 +26,21 @@ export function facadeKeyOf(entityName: string, surface?: string): string {
|
|
|
34
26
|
return surface ? `${surface}:${entityName}Handler` : `${entityName}Handler`;
|
|
35
27
|
}
|
|
36
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The address a handler answers at, read off its class name or its facade key — the dual of
|
|
31
|
+
* `facadeKeyOf`. `PostHandler` → `post`, `postHandler` → `post`.
|
|
32
|
+
*/
|
|
33
|
+
export function addressOf(name: string): string {
|
|
34
|
+
return lowerFirst(name.endsWith(HANDLER) ? name.slice(0, -HANDLER.length) : name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A default facade's key — `postHandler`, never `admin:postHandler` nor `PostStorage`. */
|
|
38
|
+
export function isFacadeKey(key: string): boolean {
|
|
39
|
+
return key.endsWith(HANDLER) && !key.includes(':');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const HANDLER = 'Handler';
|
|
43
|
+
|
|
37
44
|
/** Where the contracts behind a façade live — the dual of `facadeKeyOf`. */
|
|
38
45
|
export function contractsKeyOf(entityName: string, surface?: string): string {
|
|
39
46
|
return `${facadeKeyOf(entityName, surface)}:contracts`;
|
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
|
}
|