@fougere/core 0.12.2-alpha.0 → 0.12.5-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/EffectiveOperationModel.d.ts +1 -4
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +181 -173
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +2 -3
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +11 -13
- 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 +4 -5
- 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/Source.js +1 -1
- package/dist/Source.js.map +1 -1
- package/dist/SourceView.d.ts +0 -2
- package/dist/SourceView.d.ts.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/App.d.ts +2 -4
- package/dist/boot/App.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.d.ts +8 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js +24 -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/Extension.d.ts +6 -0
- package/dist/boot/Extension.d.ts.map +1 -1
- package/dist/boot/Hosting.d.ts +2 -1
- package/dist/boot/Hosting.d.ts.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +149 -263
- 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 +103 -114
- 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/relations.d.ts.map +1 -1
- package/dist/boot/relations.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/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/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/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/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/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/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 +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/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 +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/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/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/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/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/EffectiveOperationModel.ts +221 -201
- package/src/FougereConfig.ts +11 -16
- package/src/FrondDeclaration.ts +7 -7
- package/src/FrondsStated.ts +4 -2
- package/src/Source.ts +1 -1
- package/src/SourceView.ts +0 -2
- package/src/StatedFrond.ts +5 -0
- package/src/StatedModules.ts +17 -9
- package/src/boot/App.ts +2 -4
- package/src/boot/AppLifecycle.ts +27 -10
- package/src/boot/AuthRuntime.ts +6 -13
- package/src/boot/CreateAppOptions.ts +0 -11
- package/src/boot/Emissions.ts +3 -7
- package/src/boot/Extension.ts +6 -0
- package/src/boot/Hosting.ts +2 -1
- package/src/boot/bootstrap.ts +179 -300
- package/src/boot/install.ts +124 -146
- package/src/boot/nesting.ts +2 -1
- package/src/boot/relations.ts +3 -8
- package/src/boot/remote.ts +8 -3
- package/src/boot/seed.ts +11 -38
- package/src/descriptor/ExtensionEntry.ts +16 -1
- package/src/descriptor/FrondDescriptor.ts +7 -0
- package/src/descriptor/Fronds.ts +10 -0
- package/src/descriptor/index.ts +1 -0
- package/src/dispatch/ArgumentResolver.ts +2 -1
- package/src/dispatch/Facade.ts +3 -0
- package/src/dispatch/HandlerFacade.ts +8 -3
- package/src/dispatch/InFlight.ts +16 -2
- package/src/index.ts +2 -4
- package/src/node.ts +1 -1
- package/src/prefab/CrudConstructor.ts +0 -11
- package/src/prefab/collector.ts +29 -1
- package/src/storage/Comparison.ts +7 -4
- package/src/storage/Storage.ts +26 -12
- package/src/storage/Store.ts +4 -2
- package/src/wire/Emit.ts +1 -1
- package/src/wire/Facade.ts +17 -10
- package/src/wire/Invocation.ts +3 -1
- package/src/wire/InvocationContext.ts +2 -0
- package/src/wire/OperationContract.ts +1 -1
- package/src/wire/OperationsMap.ts +0 -10
- package/src/wire/StateShape.ts +76 -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
|
@@ -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/dispatch/InFlight.ts
CHANGED
|
@@ -36,11 +36,25 @@ export class InFlight {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
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> {
|
|
40
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
|
+
]);
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
whenIdle(): Promise<void> {
|
|
57
|
+
private whenIdle(): Promise<void> {
|
|
44
58
|
if (this.running === 0) return Promise.resolve();
|
|
45
59
|
|
|
46
60
|
return new Promise((resolve) => this.idle.push(resolve));
|
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/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';
|
|
@@ -88,15 +86,6 @@ function asCrudConstructor<T, V>(impl: object): CrudConstructor<T, V> {
|
|
|
88
86
|
return impl as CrudConstructor<T, V>;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
/** The view an op emits, fabricated. */
|
|
92
|
-
type OutOf<V, K extends CrudOpName, T> =
|
|
93
|
-
// Bracketed on purpose: a naked `V extends …` DISTRIBUTES, and the no-view default
|
|
94
|
-
// is the empty map, whose `keyof` is `never` — distribution would then collapse
|
|
95
|
-
// every op's output to `never` instead of falling through to the entity.
|
|
96
|
-
[V] extends [EntityConstructor] ? InstanceType<V & EntityConstructor>
|
|
97
|
-
: K extends keyof V ? (V[K] extends EntityConstructor ? InstanceType<V[K]> : T)
|
|
98
|
-
: T;
|
|
99
|
-
|
|
100
89
|
/** The prefab handler class — its ops, plus the statics the bootstrap and adapters read. */
|
|
101
90
|
export interface CrudConstructor<T, V = {}> {
|
|
102
91
|
// `Storage<T>`, not the bare `Storage`: a handler that injects a second storage has to
|
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
|
+
}
|
|
@@ -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
|
@@ -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
|
|
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/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
|
}
|
|
@@ -117,7 +117,7 @@ export function inferOperationKind(name: string): OperationKindInference {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** Explicit config is authoritative; otherwise the finite convention may decline. */
|
|
120
|
-
|
|
120
|
+
function resolveOperationKind(
|
|
121
121
|
name: string,
|
|
122
122
|
overrides?: Record<string, { kind?: OperationKind }>,
|
|
123
123
|
): OperationKind | undefined {
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
import type { Param } from './Param.js';
|
|
2
|
-
import type { TypeRef } from './TypeRef.js';
|
|
3
1
|
import type { OperationContract } from './OperationContract.js';
|
|
4
2
|
|
|
5
3
|
/** Map of operation name → its contract. */
|
|
6
4
|
export type OperationsMap = Map<string, OperationContract>;
|
|
7
|
-
|
|
8
|
-
export type { TypeRef, Param };
|
|
9
|
-
|
|
10
|
-
// ─── Operation intent (read vs write) ──────────
|
|
11
|
-
// Naming convention for inferred handlers — scheduled to die with the
|
|
12
|
-
// handler-kind plan (docs/notes/handler-kind.md). Lives here, NOT in
|
|
13
|
-
// @fougere/schema: it is a runtime convention about operations, not a
|
|
14
|
-
// schema concept.
|
|
@@ -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
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { SchemaView } from '@fougere/schema';
|
|
2
|
-
import type { AuthContext } from './AuthContext.js';
|
|
3
|
-
import type { AuthRuntime } from './AuthRuntime.js';
|
|
4
|
-
/** Lazy auth declaration written in fougere.config.ts. */
|
|
5
|
-
export interface AuthConfig {
|
|
6
|
-
/** Build the runtime — invoked by createApp at boot with the resolved storage handles. */
|
|
7
|
-
create(ctx: AuthContext): AuthRuntime | Promise<AuthRuntime>;
|
|
8
|
-
/**
|
|
9
|
-
* Entities the provider will use. Optional metadata — useful for the core's
|
|
10
|
-
* future migration registry to know which tables auth needs.
|
|
11
|
-
*/
|
|
12
|
-
entities?: Record<string, SchemaView>;
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=AuthConfig.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AuthConfig.d.ts","sourceRoot":"","sources":["../../src/boot/AuthConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB,0FAA0F;IAC1F,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7D;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACvC"}
|
package/dist/boot/AuthConfig.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../src/boot/AuthConfig.ts"],"names":[],"mappings":""}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { StorageFactory } from '../storage/StorageFactory.js';
|
|
2
|
-
/**
|
|
3
|
-
* Context passed by the core to an auth provider's create() function.
|
|
4
|
-
* Provides the resources the provider needs to integrate with the app.
|
|
5
|
-
*/
|
|
6
|
-
export interface AuthContext {
|
|
7
|
-
/** Storage handle (Kysely DB instance, Prisma client, etc.) — opaque to core. */
|
|
8
|
-
db: unknown;
|
|
9
|
-
/** Per-entity storage factory — auth provider uses this to back its adapter. */
|
|
10
|
-
storageFactory: StorageFactory;
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=AuthContext.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/boot/AuthContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,cAAc,EAAE,cAAc,CAAC;CAChC"}
|
package/dist/boot/AuthContext.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AuthContext.js","sourceRoot":"","sources":["../../src/boot/AuthContext.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ComparisonName.d.ts","sourceRoot":"","sources":["../../src/storage/ComparisonName.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,qFAAsF,CAAC;AAE/G,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ComparisonName.js","sourceRoot":"","sources":["../../src/storage/ComparisonName.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC"}
|
package/src/boot/AuthConfig.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { SchemaView } from '@fougere/schema';
|
|
2
|
-
import type { AuthContext } from './AuthContext.js';
|
|
3
|
-
import type { AuthRuntime } from './AuthRuntime.js';
|
|
4
|
-
|
|
5
|
-
/** Lazy auth declaration written in fougere.config.ts. */
|
|
6
|
-
export interface AuthConfig {
|
|
7
|
-
/** Build the runtime — invoked by createApp at boot with the resolved storage handles. */
|
|
8
|
-
create(ctx: AuthContext): AuthRuntime | Promise<AuthRuntime>;
|
|
9
|
-
/**
|
|
10
|
-
* Entities the provider will use. Optional metadata — useful for the core's
|
|
11
|
-
* future migration registry to know which tables auth needs.
|
|
12
|
-
*/
|
|
13
|
-
entities?: Record<string, SchemaView>;
|
|
14
|
-
}
|
package/src/boot/AuthContext.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { StorageFactory } from '../storage/StorageFactory.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Context passed by the core to an auth provider's create() function.
|
|
5
|
-
* Provides the resources the provider needs to integrate with the app.
|
|
6
|
-
*/
|
|
7
|
-
export interface AuthContext {
|
|
8
|
-
/** Storage handle (Kysely DB instance, Prisma client, etc.) — opaque to core. */
|
|
9
|
-
db: unknown;
|
|
10
|
-
/** Per-entity storage factory — auth provider uses this to back its adapter. */
|
|
11
|
-
storageFactory: StorageFactory;
|
|
12
|
-
}
|