@fougere/core 0.12.4-alpha.0 → 0.12.6-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/FougereConfig.d.ts +2 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts +5 -0
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +3 -0
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/FrondsStated.d.ts +4 -2
- package/dist/FrondsStated.d.ts.map +1 -1
- package/dist/FrondsStated.js.map +1 -1
- package/dist/StatedFrond.d.ts +3 -0
- package/dist/StatedFrond.d.ts.map +1 -1
- package/dist/StatedFrond.js +4 -0
- package/dist/StatedFrond.js.map +1 -1
- package/dist/StatedModules.d.ts +5 -2
- package/dist/StatedModules.d.ts.map +1 -1
- package/dist/StatedModules.js +11 -6
- package/dist/StatedModules.js.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +3 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +5 -0
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Extension.d.ts +6 -0
- package/dist/boot/Extension.d.ts.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js +9 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +14 -5
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/install.d.ts +3 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +1 -0
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/nesting.d.ts.map +1 -1
- package/dist/boot/nesting.js +2 -1
- package/dist/boot/nesting.js.map +1 -1
- package/dist/boot/remote.d.ts +2 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +7 -4
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts +2 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +10 -29
- package/dist/boot/seed.js.map +1 -1
- package/dist/contract.d.ts +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/descriptor/ExtensionEntry.d.ts +4 -0
- package/dist/descriptor/ExtensionEntry.d.ts.map +1 -1
- package/dist/descriptor/ExtensionEntry.js +6 -1
- package/dist/descriptor/ExtensionEntry.js.map +1 -1
- package/dist/descriptor/FrondDescriptor.d.ts +2 -0
- package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
- package/dist/descriptor/FrondDescriptor.js +5 -0
- package/dist/descriptor/FrondDescriptor.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +2 -1
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/Facade.d.ts +3 -0
- package/dist/dispatch/Facade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +8 -4
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prefab/collector.d.ts +6 -0
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js +26 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/storage/Storage.d.ts +3 -2
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js +23 -10
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +4 -2
- package/dist/storage/Store.js.map +1 -1
- package/dist/wire/BodyLimit.d.ts +14 -0
- package/dist/wire/BodyLimit.d.ts.map +1 -0
- package/dist/wire/BodyLimit.js +25 -0
- package/dist/wire/BodyLimit.js.map +1 -0
- package/dist/wire/Invocation.d.ts +2 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +4 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/InvocationContext.d.ts +2 -0
- package/dist/wire/InvocationContext.d.ts.map +1 -1
- package/dist/wire/OperationContext.d.ts +2 -0
- package/dist/wire/OperationContext.d.ts.map +1 -1
- package/dist/wire/SignedCall.d.ts +0 -2
- package/dist/wire/SignedCall.d.ts.map +1 -1
- package/dist/wire/SignedCall.js +1 -2
- package/dist/wire/SignedCall.js.map +1 -1
- package/dist/wire/StateShape.d.ts +27 -0
- package/dist/wire/StateShape.d.ts.map +1 -0
- package/dist/wire/StateShape.js +59 -0
- package/dist/wire/StateShape.js.map +1 -0
- package/package.json +4 -4
- package/src/FougereConfig.ts +2 -3
- package/src/FrondDeclaration.ts +6 -0
- package/src/FrondsStated.ts +4 -2
- package/src/StatedFrond.ts +5 -0
- package/src/StatedModules.ts +17 -9
- package/src/boot/AppLifecycle.ts +6 -0
- package/src/boot/Extension.ts +6 -0
- package/src/boot/apply.ts +11 -1
- package/src/boot/bootstrap.ts +16 -5
- package/src/boot/install.ts +4 -0
- package/src/boot/nesting.ts +2 -1
- package/src/boot/remote.ts +8 -3
- package/src/boot/seed.ts +11 -36
- package/src/contract.ts +1 -1
- package/src/descriptor/ExtensionEntry.ts +16 -1
- package/src/descriptor/FrondDescriptor.ts +7 -0
- package/src/dispatch/ArgumentResolver.ts +2 -1
- package/src/dispatch/Facade.ts +3 -0
- package/src/dispatch/HandlerFacade.ts +8 -3
- package/src/index.ts +1 -1
- package/src/prefab/collector.ts +29 -1
- package/src/storage/Storage.ts +26 -12
- package/src/storage/Store.ts +4 -2
- package/src/wire/BodyLimit.ts +28 -0
- package/src/wire/Invocation.ts +3 -1
- package/src/wire/InvocationContext.ts +2 -0
- package/src/wire/OperationContext.ts +2 -0
- package/src/wire/SignedCall.ts +0 -3
- package/src/wire/StateShape.ts +76 -0
|
@@ -88,7 +88,10 @@ export class HandlerFacade {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const received = Invocation.from(input);
|
|
92
|
+
const state = received.crossed ? { ...received.state } : this.facade.state.judge(received.state, entity, op);
|
|
93
|
+
const invocation = received.withState(state);
|
|
94
|
+
const entered = { ...state };
|
|
92
95
|
const context: OperationContext = {
|
|
93
96
|
entity,
|
|
94
97
|
frond: this.facade.frond,
|
|
@@ -98,7 +101,7 @@ export class HandlerFacade {
|
|
|
98
101
|
invocation,
|
|
99
102
|
};
|
|
100
103
|
|
|
101
|
-
return runMiddlewares(this.facade.middlewares(), context, () => this.answer(op, contract, context, invocation));
|
|
104
|
+
return runMiddlewares(this.facade.middlewares(), context, () => this.answer(op, contract, context, invocation, entered));
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -110,8 +113,10 @@ export class HandlerFacade {
|
|
|
110
113
|
contract: OperationContract,
|
|
111
114
|
context: OperationContext,
|
|
112
115
|
invocation: Invocation,
|
|
116
|
+
entered: Record<string, unknown>,
|
|
113
117
|
): Promise<unknown> {
|
|
114
|
-
const
|
|
118
|
+
const state = this.facade.state.judge(context.state, this.handler.address, op, entered);
|
|
119
|
+
const validated = validateInput(contract.input, { ...invocation, state }, this.handler.address, op);
|
|
115
120
|
context.invocation = validated;
|
|
116
121
|
|
|
117
122
|
const args = contract.binding ? await this.arguments.resolve(contract.binding, validated) : [];
|
package/src/index.ts
CHANGED
|
@@ -62,7 +62,7 @@ export { FougereError, ErrorCode, validationErrorsOf } from './contract.js';
|
|
|
62
62
|
export type { AppMiddleware } from './wire/AppMiddleware.js';
|
|
63
63
|
export type { AppNext } from './wire/AppNext.js';
|
|
64
64
|
export type { OperationContext } from './wire/OperationContext.js';
|
|
65
|
-
export { assertIdentityCard, RPC_ENTITY,
|
|
65
|
+
export { assertIdentityCard, RPC_ENTITY, ENVELOPE_BYTES, maxBodyBytes, maxFrameBytes, setMaxBodyBytes } from './contract.js';
|
|
66
66
|
export { createLocalRunner, createAppRunner } from './boot/runner.js';
|
|
67
67
|
|
|
68
68
|
// A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
|
package/src/prefab/collector.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Documented: [collectors](https://fougere.dev/docs/business/collectors).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { upperFirst } from '@fougere/schema';
|
|
7
|
+
import { FieldValueValidator, dotted, json, upperFirst, type Field, type SchemaView } from '@fougere/schema';
|
|
8
|
+
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
9
|
+
import { FougereError } from '../wire/FougereError.js';
|
|
8
10
|
|
|
9
11
|
export function Collector<T extends abstract new (...args: any[]) => any>(target: T) {
|
|
10
12
|
class CollectorBase {
|
|
@@ -18,3 +20,29 @@ export function Collector<T extends abstract new (...args: any[]) => any>(target
|
|
|
18
20
|
export function collectorKeyOf(type: string): string {
|
|
19
21
|
return `${upperFirst(type)}Collector`;
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
const readers = new WeakMap<object, Field>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What a collector answered, read by the entity it names — judged and rebuilt, so a `User` that
|
|
28
|
+
* crossed a process as JSON reaches the handler with its dates. A collector naming no entity
|
|
29
|
+
* answers what it built.
|
|
30
|
+
*/
|
|
31
|
+
export function collectedAs(collector: object, value: unknown): unknown {
|
|
32
|
+
const target = (collector.constructor as { __entity?: Partial<SchemaView> }).__entity;
|
|
33
|
+
if (value === null || value === undefined || typeof target?.getFields !== 'function') return value;
|
|
34
|
+
|
|
35
|
+
let reader = readers.get(target);
|
|
36
|
+
if (!reader) {
|
|
37
|
+
reader = json(target as SchemaView & (new () => unknown));
|
|
38
|
+
readers.set(target, reader);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const verdict = FieldValueValidator.of(reader).parse(value);
|
|
42
|
+
if ('value' in verdict) return verdict.value;
|
|
43
|
+
|
|
44
|
+
throw new FougereError({
|
|
45
|
+
code: ErrorCode.VALIDATION_FAILED,
|
|
46
|
+
message: `${collector.constructor.name}: ${verdict.path?.length ? `${dotted(verdict.path)}: ` : ''}${verdict.message}`,
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/storage/Storage.ts
CHANGED
|
@@ -2,15 +2,21 @@ import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
|
|
|
2
2
|
import type { ListOptions } from './ListOptions.js';
|
|
3
3
|
import type { ListResult } from './ListResult.js';
|
|
4
4
|
import type { SelectOption } from './SelectOption.js';
|
|
5
|
+
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
6
|
+
import { FougereError } from '../wire/FougereError.js';
|
|
5
7
|
|
|
6
|
-
/** The keys `list()` answers to. */
|
|
7
|
-
const LIST_OPTION_KEYS =
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** The keys `list()` answers to, checked against `ListOptions` so neither can drift. */
|
|
9
|
+
const LIST_OPTION_KEYS = Object.keys({
|
|
10
|
+
limit: true, offset: true, page: true, after: true, orderBy: true, order: true, count: true, where: true, select: true,
|
|
11
|
+
} satisfies Record<keyof (ListOptions & SelectOption), true>);
|
|
12
|
+
|
|
13
|
+
/** What a count must be — a text that did not read as a number arrives as `NaN`. */
|
|
14
|
+
const COUNTS = ['limit', 'offset', 'page'] as const;
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
|
-
* Refuse an option the port does not answer to, and an order by a
|
|
13
|
-
* declare — an unhonoured `orderBy` returns the page in whatever order
|
|
17
|
+
* Refuse an option the port does not answer to, a count that is not one, and an order by a
|
|
18
|
+
* field the entity does not declare — an unhonoured `orderBy` returns the page in whatever order
|
|
19
|
+
* the engine chose. Refused as BAD_REQUEST: a plain Error left as INTERNAL_ERROR, its message hidden.
|
|
14
20
|
*/
|
|
15
21
|
export function assertListOptions(
|
|
16
22
|
options: object | undefined,
|
|
@@ -18,21 +24,29 @@ export function assertListOptions(
|
|
|
18
24
|
declared: readonly string[],
|
|
19
25
|
): void {
|
|
20
26
|
if (!options) return;
|
|
21
|
-
const
|
|
22
|
-
const strangers = Object.keys(options).filter((key) => !legal.has(key));
|
|
27
|
+
const strangers = Object.keys(options).filter((key) => !LIST_OPTION_KEYS.includes(key));
|
|
23
28
|
if (strangers.length > 0) {
|
|
24
|
-
|
|
29
|
+
refuse(
|
|
25
30
|
`${entity}.list(): unknown option ${strangers.map((s) => `\`${s}\``).join(', ')}. ` +
|
|
26
31
|
`Known options are ${LIST_OPTION_KEYS.join(', ')} — to filter, pass \`where: { ${strangers[0]}: … }\`.`,
|
|
27
32
|
);
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
for (const count of COUNTS) {
|
|
36
|
+
const value = (options as ListOptions)[count];
|
|
37
|
+
if (value !== undefined && !(Number.isInteger(value) && value >= 0)) {
|
|
38
|
+
refuse(`${entity}.list(): \`${count}\` is ${typeof value === 'number' ? value : JSON.stringify(value)}, not a whole number.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
const { orderBy } = options as ListOptions;
|
|
31
43
|
if (orderBy === undefined || declared.includes(orderBy)) return;
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
refuse(`${entity}.list(): unknown orderBy \`${orderBy}\`. ${entity} declares ${declared.join(', ')}.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function refuse(message: string): never {
|
|
49
|
+
throw new FougereError({ code: ErrorCode.BAD_REQUEST, message });
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/** Per-entity storage — scoped CRUD operations on a single entity type. */
|
package/src/storage/Store.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
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
|
+
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
5
|
+
import { FougereError } from '../wire/FougereError.js';
|
|
4
6
|
import type { StorageFactory } from './StorageFactory.js';
|
|
5
7
|
import type { Values } from './Values.js';
|
|
6
8
|
import type { ListResult } from './ListResult.js';
|
|
@@ -128,7 +130,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
128
130
|
// constraint violation, and a store that loses data silently is worse than one
|
|
129
131
|
// that fails.
|
|
130
132
|
if (await store.has(keyOf(id))) {
|
|
131
|
-
throw new
|
|
133
|
+
throw new FougereError({ code: ErrorCode.CONFLICT, message: `${name}.create: '${pk}' ${JSON.stringify(id)} already exists.` });
|
|
132
134
|
}
|
|
133
135
|
await store.set(keyOf(id), values);
|
|
134
136
|
|
|
@@ -136,7 +138,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
136
138
|
},
|
|
137
139
|
async update(id: string, input: Partial<Record<string, unknown>>) {
|
|
138
140
|
const existing = await store.get(keyOf(id));
|
|
139
|
-
if (!existing) throw new
|
|
141
|
+
if (!existing) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: `No row in ${name} for ${JSON.stringify(id)}.` });
|
|
140
142
|
const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
|
|
141
143
|
await store.set(keyOf(id), updated);
|
|
142
144
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** How much a caller may send — held ONCE for the process and CONSULTED at every door, like the log level. */
|
|
2
|
+
let limit = 2 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
/** The room a hop keeps for what it adds around a call — trace, identity, state — which never counts against the caller. */
|
|
5
|
+
export const ENVELOPE_BYTES = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
export function maxBodyBytes(): number {
|
|
8
|
+
return limit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Set by `applyConfig` from `maxBodyBytes:`. */
|
|
12
|
+
export function setMaxBodyBytes(bytes: number): void {
|
|
13
|
+
if (!Number.isInteger(bytes) || bytes <= 0) {
|
|
14
|
+
throw new Error(`maxBodyBytes: a positive whole number of bytes — got ${String(bytes)}.`);
|
|
15
|
+
}
|
|
16
|
+
limit = bytes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a process forwarding a call may frame and a receiver may read: the caller's limit, and the
|
|
21
|
+
* envelope's room. Measured against the caller's limit alone, a body just under it passed in
|
|
22
|
+
* process and was refused a hop away.
|
|
23
|
+
*
|
|
24
|
+
* Documented: [gradient](https://fougere.dev/docs/infra/gradient).
|
|
25
|
+
*/
|
|
26
|
+
export function maxFrameBytes(): number {
|
|
27
|
+
return limit + ENVELOPE_BYTES;
|
|
28
|
+
}
|
package/src/wire/Invocation.ts
CHANGED
|
@@ -36,8 +36,9 @@ export class Invocation implements InvocationContext {
|
|
|
36
36
|
readonly identity?: string;
|
|
37
37
|
readonly caller?: string;
|
|
38
38
|
readonly runAt?: number;
|
|
39
|
+
readonly crossed?: true;
|
|
39
40
|
|
|
40
|
-
/** Gives every entry and every transport the same
|
|
41
|
+
/** Gives every entry and every transport the same nine-member value, frozen. */
|
|
41
42
|
private constructor(context: PartialInvocation) {
|
|
42
43
|
this.params = canonicalRecord(context.params);
|
|
43
44
|
this.query = canonicalRecord(context.query);
|
|
@@ -49,6 +50,7 @@ export class Invocation implements InvocationContext {
|
|
|
49
50
|
if (context.identity !== undefined) this.identity = context.identity;
|
|
50
51
|
if (context.caller !== undefined) this.caller = context.caller;
|
|
51
52
|
if (context.runAt !== undefined) this.runAt = context.runAt;
|
|
53
|
+
if (context.crossed) this.crossed = true;
|
|
52
54
|
Object.freeze(this);
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -8,4 +8,6 @@ export interface InvocationContext {
|
|
|
8
8
|
caller?: string;
|
|
9
9
|
/** Epoch milliseconds before which this call is not to run. */
|
|
10
10
|
runAt?: number;
|
|
11
|
+
/** Written by the receiver of a call from another process, whose state was judged where it entered. */
|
|
12
|
+
crossed?: true;
|
|
11
13
|
}
|
|
@@ -15,4 +15,6 @@ export interface OperationContext {
|
|
|
15
15
|
state: Record<string, unknown>;
|
|
16
16
|
/** Transport-agnostic invocation context (params, query, input, state). */
|
|
17
17
|
invocation?: import('./InvocationContext.js').InvocationContext;
|
|
18
|
+
/** Set by the facade that hands the call to a transport — the dual of `invocation.crossed`. */
|
|
19
|
+
crosses?: true;
|
|
18
20
|
}
|
package/src/wire/SignedCall.ts
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { InputValidator, dotted, optional, type Field, type Fields } from '@fougere/schema';
|
|
2
|
+
import { ErrorCode } from './ErrorCode.js';
|
|
3
|
+
import { FougereError } from './FougereError.js';
|
|
4
|
+
|
|
5
|
+
/** An extension and the members it puts on a call's `state`. */
|
|
6
|
+
export interface StateDeclaration {
|
|
7
|
+
name: string;
|
|
8
|
+
state?: Readonly<Record<string, Field>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What a call's `state` may hold in this process: the members its extensions declare, each one
|
|
13
|
+
* absent until a host fills it. Judged at the facade, so a member arriving over the wire is
|
|
14
|
+
* rebuilt by its field exactly as it would have been handed over in-process.
|
|
15
|
+
*
|
|
16
|
+
* Documented: [collectors](https://fougere.dev/docs/business/collectors).
|
|
17
|
+
*/
|
|
18
|
+
export class StateShape {
|
|
19
|
+
static readonly empty = new StateShape({});
|
|
20
|
+
|
|
21
|
+
private readonly validator: InputValidator;
|
|
22
|
+
|
|
23
|
+
private constructor(private readonly fields: Fields) {
|
|
24
|
+
this.validator = InputValidator.of(fields);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Two extensions declaring one member refuse: the field that judges it would depend on wiring order. */
|
|
28
|
+
static of(declarations: readonly StateDeclaration[]): StateShape {
|
|
29
|
+
const fields: Fields = {};
|
|
30
|
+
const owners = new Map<string, string>();
|
|
31
|
+
for (const { name, state } of declarations) {
|
|
32
|
+
for (const [member, field] of Object.entries(state ?? {})) {
|
|
33
|
+
const owner = owners.get(member);
|
|
34
|
+
if (owner) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[state] '${member}' is declared by two extensions, '${owner}' and '${name}'.\n`
|
|
37
|
+
+ ' A member has one owner — keep one of them out of `extensions:`.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
owners.set(member, name);
|
|
41
|
+
fields[member] = optional(field);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new StateShape(fields);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The members written since `entered` rebuilt by their fields, or a refusal naming the member
|
|
50
|
+
* and what this process declares. What `entered` already held was judged where it entered.
|
|
51
|
+
*/
|
|
52
|
+
judge(
|
|
53
|
+
state: Record<string, unknown>,
|
|
54
|
+
entity: string,
|
|
55
|
+
operation: string,
|
|
56
|
+
entered: Record<string, unknown> = {},
|
|
57
|
+
): Record<string, unknown> {
|
|
58
|
+
const written = Object.fromEntries(Object.entries(state).filter(([member, value]) => entered[member] !== value));
|
|
59
|
+
if (Object.keys(written).length === 0) return state;
|
|
60
|
+
|
|
61
|
+
const result = this.validator.validate(written);
|
|
62
|
+
if (result.success) return { ...state, ...result.data };
|
|
63
|
+
|
|
64
|
+
const declared = Object.keys(this.fields);
|
|
65
|
+
const errors = result.errors.map((error) => ({ ...error, path: ['state', ...error.path] }));
|
|
66
|
+
|
|
67
|
+
throw new FougereError({
|
|
68
|
+
code: ErrorCode.VALIDATION_FAILED,
|
|
69
|
+
message: errors.map((error) => `${dotted(error.path)}: ${error.message}`).join(', ')
|
|
70
|
+
+ (declared.length ? ` — this process declares ${declared.join(', ')}` : ' — this process declares no state member'),
|
|
71
|
+
details: errors,
|
|
72
|
+
entity,
|
|
73
|
+
operation,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|