@fougere/core 0.8.4-alpha.0 → 0.9.0-alpha.1
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/boot/AppLifecycle.d.ts +8 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts +26 -0
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +143 -8
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +83 -18
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js +6 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +1 -0
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/install.d.ts +2 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +33 -4
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ports.d.ts +12 -3
- package/dist/boot/ports.d.ts.map +1 -1
- package/dist/boot/ports.js +51 -21
- package/dist/boot/ports.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +3 -3
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/types.d.ts +16 -1
- package/dist/boot/types.d.ts.map +1 -1
- package/dist/builtin/LogLine.d.ts +38 -0
- package/dist/builtin/LogLine.d.ts.map +1 -0
- package/dist/builtin/LogLine.js +37 -0
- package/dist/builtin/LogLine.js.map +1 -0
- package/dist/builtin/logger.d.ts +57 -0
- package/dist/builtin/logger.d.ts.map +1 -1
- package/dist/builtin/logger.js +72 -17
- package/dist/builtin/logger.js.map +1 -1
- package/dist/config-loader.d.ts +2 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js.map +1 -1
- package/dist/contract.d.ts +3 -7
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +3 -4
- package/dist/contract.js.map +1 -1
- package/dist/conventions.d.ts +6 -0
- package/dist/conventions.d.ts.map +1 -1
- package/dist/conventions.js +3 -2
- package/dist/conventions.js.map +1 -1
- package/dist/declare.d.ts +11 -1
- package/dist/declare.d.ts.map +1 -1
- package/dist/declare.js +9 -0
- package/dist/declare.js.map +1 -1
- package/dist/descriptor/frond.d.ts +34 -0
- package/dist/descriptor/frond.d.ts.map +1 -1
- package/dist/descriptor/index.d.ts +1 -1
- package/dist/descriptor/index.d.ts.map +1 -1
- package/dist/descriptor/index.js +1 -1
- package/dist/descriptor/index.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +6 -2
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +2 -2
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +11 -7
- package/dist/effective-operation.js.map +1 -1
- package/dist/frond-config.d.ts +24 -0
- package/dist/frond-config.d.ts.map +1 -1
- package/dist/frond-config.js.map +1 -1
- package/dist/index.d.ts +30 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/wire/Invocation.d.ts +1 -3
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +1 -5
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/binding.d.ts +9 -0
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +11 -6
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/emit.d.ts +35 -3
- package/dist/wire/emit.d.ts.map +1 -1
- package/dist/wire/emit.js +26 -3
- package/dist/wire/emit.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js +6 -0
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +4 -4
- package/src/boot/AppLifecycle.ts +8 -0
- package/src/boot/Emissions.ts +176 -8
- package/src/boot/bootstrap.ts +95 -17
- package/src/boot/card.ts +6 -1
- package/src/boot/hosted.ts +3 -1
- package/src/boot/install.ts +35 -4
- package/src/boot/ports.ts +71 -27
- package/src/boot/remote.ts +3 -3
- package/src/boot/types.ts +16 -1
- package/src/builtin/LogLine.ts +38 -0
- package/src/builtin/logger.ts +109 -17
- package/src/config-loader.ts +2 -1
- package/src/contract.ts +5 -9
- package/src/conventions.ts +9 -2
- package/src/declare.ts +19 -1
- package/src/descriptor/frond.ts +31 -0
- package/src/descriptor/index.ts +1 -1
- package/src/dispatch/ArgumentResolver.ts +6 -2
- package/src/dispatch/HandlerFacade.ts +2 -2
- package/src/effective-operation.ts +13 -7
- package/src/frond-config.ts +20 -0
- package/src/index.ts +36 -50
- package/src/wire/Invocation.ts +2 -7
- package/src/wire/binding.ts +17 -6
- package/src/wire/emit.ts +58 -6
- package/src/wire/loggerMiddleware.ts +6 -0
package/src/builtin/logger.ts
CHANGED
|
@@ -18,6 +18,54 @@ export type LogSink = (record: LogRecord) => void;
|
|
|
18
18
|
/** Who else takes this process's log lines, beside the console. */
|
|
19
19
|
const sinks: LogSink[] = [];
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Where one boot's lines wait, and where they go once they can.
|
|
23
|
+
*
|
|
24
|
+
* PER BOOT and never per process: two apps in one process each have their own
|
|
25
|
+
* destinations, and a slot shared between them sent the second app's lines to the first
|
|
26
|
+
* app's door — measured on `demos/observability`, where only the first of three printed.
|
|
27
|
+
*
|
|
28
|
+
* A boot writes most of what a process ever logs, and it writes it before any emission is
|
|
29
|
+
* registered, so the lines that say what an app is made of are the ones a destination
|
|
30
|
+
* would miss. Bounded: a boot that never finishes must not grow, and what is dropped is
|
|
31
|
+
* the OLDEST since the lines explaining a refusal are the last.
|
|
32
|
+
*/
|
|
33
|
+
export class Carry {
|
|
34
|
+
private held: LogRecord[] = [];
|
|
35
|
+
private take?: (line: LogRecord) => void;
|
|
36
|
+
|
|
37
|
+
static readonly MAX = 500;
|
|
38
|
+
|
|
39
|
+
/** Take it now, or keep it for whoever arrives. */
|
|
40
|
+
push(record: LogRecord): void {
|
|
41
|
+
if (this.take) {
|
|
42
|
+
try {
|
|
43
|
+
this.take(record);
|
|
44
|
+
} catch { /* announcing never breaks logging, for the same reason a sink does not */ }
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.held.push(record);
|
|
48
|
+
if (this.held.length > Carry.MAX) this.held.shift();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Hand what is held over, and everything after it. */
|
|
52
|
+
to(take: (line: LogRecord) => void): () => void {
|
|
53
|
+
this.take = take;
|
|
54
|
+
for (const line of this.held.splice(0)) take(line);
|
|
55
|
+
|
|
56
|
+
return () => { this.take = undefined; };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Forget what is still held — a boot that refused, or an app that declares no
|
|
61
|
+
* destination. Nothing is printed: the console had every one of these lines when it was
|
|
62
|
+
* written, and the hold exists only to hand them on later.
|
|
63
|
+
*/
|
|
64
|
+
forget(): void {
|
|
65
|
+
this.held.length = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
/** Take every line this process logs. Returns the way to withdraw. */
|
|
22
70
|
export function onLog(next: LogSink): () => void {
|
|
23
71
|
sinks.push(next);
|
|
@@ -79,8 +127,8 @@ function supportsColor(): boolean {
|
|
|
79
127
|
return false;
|
|
80
128
|
}
|
|
81
129
|
|
|
82
|
-
function
|
|
83
|
-
const d = new Date();
|
|
130
|
+
function stamp(at: number | Date): string {
|
|
131
|
+
const d = new Date(at);
|
|
84
132
|
const h = String(d.getHours()).padStart(2, '0');
|
|
85
133
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
86
134
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
@@ -91,6 +139,12 @@ function formatTime(): string {
|
|
|
91
139
|
export interface LoggerOptions {
|
|
92
140
|
/** Logger name / prefix. */
|
|
93
141
|
name?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Where its lines go — one boot's, so two apps in a process do not share a door. A
|
|
144
|
+
* logger without one writes to the console and nowhere else, which is what the boot's
|
|
145
|
+
* first lines do and what an app declaring no destination does forever.
|
|
146
|
+
*/
|
|
147
|
+
carry?: Carry;
|
|
94
148
|
/** Force color on/off. Auto-detected by default. */
|
|
95
149
|
color?: boolean;
|
|
96
150
|
}
|
|
@@ -98,15 +152,17 @@ export interface LoggerOptions {
|
|
|
98
152
|
export class Logger {
|
|
99
153
|
private name: string;
|
|
100
154
|
private color: boolean;
|
|
155
|
+
private carry?: Carry;
|
|
101
156
|
|
|
102
157
|
constructor(prefix?: string, options?: Omit<LoggerOptions, 'name'>) {
|
|
103
158
|
this.name = prefix ?? 'app';
|
|
104
159
|
this.color = options?.color ?? supportsColor();
|
|
160
|
+
this.carry = options?.carry;
|
|
105
161
|
}
|
|
106
162
|
|
|
107
163
|
/** Create a child logger with a sub-name. It carries no level of its own either. */
|
|
108
164
|
child(name: string): Logger {
|
|
109
|
-
return new Logger(`${this.name}:${name}`, { color: this.color });
|
|
165
|
+
return new Logger(`${this.name}:${name}`, { color: this.color, ...(this.carry ? { carry: this.carry } : {}) });
|
|
110
166
|
}
|
|
111
167
|
|
|
112
168
|
debug(msg: string, ...args: unknown[]) { this.log('debug', msg, args); }
|
|
@@ -116,27 +172,63 @@ export class Logger {
|
|
|
116
172
|
|
|
117
173
|
private log(level: string, msg: string, args: unknown[]) {
|
|
118
174
|
if (LEVELS[level as LogLevel] < threshold) return;
|
|
175
|
+
const record: LogRecord = {
|
|
176
|
+
level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
|
|
177
|
+
};
|
|
119
178
|
|
|
120
179
|
// Beside the console, never instead of it: a forwarded line is an addition, and a
|
|
121
180
|
// sink that throws must not cost the operator the line they were reading.
|
|
122
181
|
for (const take of sinks) {
|
|
123
182
|
try {
|
|
124
|
-
take(
|
|
183
|
+
take(record);
|
|
125
184
|
} catch { /* forwarding never breaks logging */ }
|
|
126
185
|
}
|
|
127
186
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const prefix = `${COLORS.dim}${time}${COLORS.reset} ${c}${COLORS.bold}${style.badge}${COLORS.reset} ${COLORS.magenta}${this.name}${COLORS.reset}`;
|
|
137
|
-
console[method](prefix, msg, ...args);
|
|
138
|
-
} else {
|
|
139
|
-
console[method](`${time} ${style.badge} [${this.name}]`, msg, ...args);
|
|
140
|
-
}
|
|
187
|
+
this.carry?.push(record);
|
|
188
|
+
|
|
189
|
+
// The console ALWAYS, whoever else took the line. Skipping it once a destination
|
|
190
|
+
// existed made `calls()` — a devtools ring that prints nothing — silence the
|
|
191
|
+
// operator's terminal: 306 per-operation lines in `demos/observability` became 2.
|
|
192
|
+
// A destination sends a line ELSEWHERE; it does not take over stderr.
|
|
193
|
+
const { method, text } = formatted(record, this.color);
|
|
194
|
+
console[method](...text);
|
|
141
195
|
}
|
|
142
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One line, ready for a terminal — the console arguments and which method takes them.
|
|
200
|
+
*
|
|
201
|
+
* Here rather than inside the class because the boot is not the only writer: a destination
|
|
202
|
+
* that prints (`@fougere/log`) hands its own record to the same formatting, so the two
|
|
203
|
+
* outputs cannot drift.
|
|
204
|
+
*
|
|
205
|
+
* One console method per level: `debug` and `info` both went to `console.log`, so nothing
|
|
206
|
+
* downstream — a terminal filter, a collector — could tell them apart.
|
|
207
|
+
*/
|
|
208
|
+
export interface Rendered {
|
|
209
|
+
level: Exclude<LogLevel, 'silent'>;
|
|
210
|
+
name: string;
|
|
211
|
+
message: string;
|
|
212
|
+
/** Absent on most lines: a message usually carries its own detail. */
|
|
213
|
+
args?: unknown[] | null;
|
|
214
|
+
/** Epoch milliseconds from a logger, a `Date` from an entity that stamped it. */
|
|
215
|
+
at: number | Date;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function formatted(
|
|
219
|
+
record: Rendered,
|
|
220
|
+
color = supportsColor(),
|
|
221
|
+
): { method: Rendered['level']; text: unknown[] } {
|
|
222
|
+
const style = LEVEL_STYLE[record.level];
|
|
223
|
+
const time = stamp(record.at);
|
|
224
|
+
const method = record.level;
|
|
225
|
+
|
|
226
|
+
if (color) {
|
|
227
|
+
const c = COLORS[style.color];
|
|
228
|
+
const prefix = `${COLORS.dim}${time}${COLORS.reset} ${c}${COLORS.bold}${style.badge}${COLORS.reset} ${COLORS.magenta}${record.name}${COLORS.reset}`;
|
|
229
|
+
|
|
230
|
+
return { method, text: [prefix, record.message, ...(record.args ?? [])] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { method, text: [`${time} ${style.badge} [${record.name}]`, record.message, ...(record.args ?? [])] };
|
|
234
|
+
}
|
package/src/config-loader.ts
CHANGED
|
@@ -22,7 +22,8 @@ export interface FougereConfig {
|
|
|
22
22
|
/** Remote fronds — frondName → base URL. */
|
|
23
23
|
remotes?: Record<string, string>;
|
|
24
24
|
/** Which realization answers which port — port class name → implementation class name. */
|
|
25
|
-
|
|
25
|
+
/** What answers a port — a name, or the chain from the outside in. */
|
|
26
|
+
ports?: Record<string, string | readonly string[]>;
|
|
26
27
|
/** Auth declaration — picks a provider package and forwards options to it. */
|
|
27
28
|
auth?: AuthConfig;
|
|
28
29
|
/** Which protocol adapters this app serves. */
|
package/src/contract.ts
CHANGED
|
@@ -3,25 +3,21 @@
|
|
|
3
3
|
* runtime: the error vocabulary, the invocation shape, the call value, the naming convention.
|
|
4
4
|
*/
|
|
5
5
|
import { lowerFirst } from '@fougere/schema';
|
|
6
|
-
import {
|
|
6
|
+
import { Invocation, type InvocationContext } from './wire/Invocation.js';
|
|
7
7
|
import type { FrondCall } from './wire/call.js';
|
|
8
8
|
|
|
9
9
|
export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
|
|
10
10
|
// What a read may ask about one field. Here rather than on the main entry because an
|
|
11
11
|
// adapter reads it to compile a query, and an adapter carries no boot.
|
|
12
|
-
export { comparisonOf, comparisonsIn
|
|
13
|
-
export type { Comparison
|
|
12
|
+
export { comparisonOf, comparisonsIn } from './storage/criterion.js';
|
|
13
|
+
export type { Comparison } from './storage/criterion.js';
|
|
14
14
|
// A receiver turns a refusal into what may cross a process boundary, and it is not
|
|
15
15
|
// core's boot — reaching it through the main entry dragged the scanner into a bundle.
|
|
16
16
|
export { toPublicError } from './wire/http-error.js';
|
|
17
|
-
export type { FougereErrorOptions } from './wire/errors.js';
|
|
18
|
-
export { canonicalInvocation, EMPTY_INVOCATION } from './wire/Invocation.js';
|
|
19
|
-
export type { InvocationContext } from './wire/Invocation.js';
|
|
20
17
|
export { Invocation } from './wire/Invocation.js';
|
|
21
|
-
export type {
|
|
18
|
+
export type { InvocationContext } from './wire/Invocation.js';
|
|
22
19
|
export { Call } from './wire/call.js';
|
|
23
20
|
export { RouteAddress } from './wire/RouteAddress.js';
|
|
24
|
-
export type { RouteAddressInput } from './wire/RouteAddress.js';
|
|
25
21
|
export type { FrondCall, Transport, SignedCall } from './wire/call.js';
|
|
26
22
|
export { MAX_BODY_BYTES } from './wire/call.js';
|
|
27
23
|
// The reserved entity, VALUE and not type: a consumer that wants to leave it alone — a
|
|
@@ -59,5 +55,5 @@ export function callValueOf(
|
|
|
59
55
|
typeof opOrInput === 'string'
|
|
60
56
|
? [{ entity: lowerFirst((target as { name: string }).name), op: opOrInput }, input]
|
|
61
57
|
: [target as FrondCall, opOrInput];
|
|
62
|
-
return { call, invocation:
|
|
58
|
+
return { call, invocation: Invocation.from(given) };
|
|
63
59
|
}
|
package/src/conventions.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface Conventions {
|
|
|
14
14
|
presenters: string;
|
|
15
15
|
collectors: string;
|
|
16
16
|
seeds: string;
|
|
17
|
+
/**
|
|
18
|
+
* What runs around this frond's operations. Scoped to the frond that declares it,
|
|
19
|
+
* unless `frond.config.ts` widens it — the one directory whose members apply to code
|
|
20
|
+
* they do not name.
|
|
21
|
+
*/
|
|
22
|
+
middlewares: string;
|
|
17
23
|
/**
|
|
18
24
|
* What the domain computes with nothing injected. Read into the type program, because
|
|
19
25
|
* a handler names one of its types in a signature, and registered nowhere: no class
|
|
@@ -37,6 +43,7 @@ export const DEFAULT_CONVENTIONS: Conventions = {
|
|
|
37
43
|
presenters: 'presenters',
|
|
38
44
|
collectors: 'collectors',
|
|
39
45
|
seeds: 'seeds',
|
|
46
|
+
middlewares: 'middlewares',
|
|
40
47
|
rules: 'rules',
|
|
41
48
|
versions: 'versions',
|
|
42
49
|
},
|
|
@@ -70,9 +77,9 @@ export function providerDirsOf(conventions: Conventions): string[] {
|
|
|
70
77
|
|
|
71
78
|
/** The frond vocabulary — every directory the scan reads. */
|
|
72
79
|
export function frondDirsOf(conventions: Conventions): string[] {
|
|
73
|
-
const { entities, handlers, presenters, collectors, seeds, rules, versions } = conventions.dirs;
|
|
80
|
+
const { entities, handlers, presenters, collectors, seeds, middlewares, rules, versions } = conventions.dirs;
|
|
74
81
|
return [...new Set([
|
|
75
|
-
entities, handlers, presenters, collectors, seeds, rules, versions,
|
|
82
|
+
entities, handlers, presenters, collectors, seeds, middlewares, rules, versions,
|
|
76
83
|
...providerDirsOf(conventions),
|
|
77
84
|
])];
|
|
78
85
|
}
|
package/src/declare.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** A frond stated by its author, for an app that will not scan. */
|
|
2
2
|
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
3
3
|
import type {
|
|
4
|
-
CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry,
|
|
4
|
+
CollectorEntry, EntityEntry, FrondDescriptor, HandlerEntry, MiddlewareEntry,
|
|
5
5
|
PresenterEntry, ProviderEntry, SeedEntry,
|
|
6
6
|
} from './descriptor/frond.js';
|
|
7
7
|
import { DEFAULT_CONVENTIONS } from './conventions.js';
|
|
@@ -90,6 +90,14 @@ export interface FrondDeclaration {
|
|
|
90
90
|
collectors?: (Ctor | DeclaredSubject)[];
|
|
91
91
|
providers?: Declared[];
|
|
92
92
|
seeds?: { entityName: string; data: SeedEntry['data'] }[];
|
|
93
|
+
/**
|
|
94
|
+
* What runs around this frond's operations. The scope is stated beside the class here,
|
|
95
|
+
* where `frond.config.ts` states it by class name — a statement has no config file to
|
|
96
|
+
* put it in.
|
|
97
|
+
*/
|
|
98
|
+
middlewares?: (Ctor | (DeclaredSubject & { scope?: MiddlewareEntry['scope'] }))[];
|
|
99
|
+
/** The ops that finish a fact, in order — the same key `frond.config.ts` states. */
|
|
100
|
+
pipes?: Record<string, string[]>;
|
|
93
101
|
/** Per-surface entity lists — the same key `frond.config.ts` states. */
|
|
94
102
|
surfaces?: Record<string, string[]>;
|
|
95
103
|
/** The import scope this frond answers under. Defaults to the conventional one. */
|
|
@@ -179,6 +187,14 @@ export function frond(name: string, declared: FrondDeclaration = {}): FrondDescr
|
|
|
179
187
|
filePath: '',
|
|
180
188
|
}));
|
|
181
189
|
|
|
190
|
+
const middlewares: MiddlewareEntry[] = (declared.middlewares ?? []).map((m) => ({
|
|
191
|
+
name: ctorOf(m).name,
|
|
192
|
+
ctor: ctorOf(m),
|
|
193
|
+
scope: (typeof m === 'function' ? undefined : m.scope) ?? 'frond',
|
|
194
|
+
deps: depsOf(m),
|
|
195
|
+
filePath: '',
|
|
196
|
+
}));
|
|
197
|
+
|
|
182
198
|
return {
|
|
183
199
|
name,
|
|
184
200
|
source: { path: '', package: `${scope}/${name}` },
|
|
@@ -189,6 +205,8 @@ export function frond(name: string, declared: FrondDeclaration = {}): FrondDescr
|
|
|
189
205
|
presenters,
|
|
190
206
|
collectors,
|
|
191
207
|
seeds,
|
|
208
|
+
middlewares,
|
|
209
|
+
...(declared.pipes ? { pipes: declared.pipes } : {}),
|
|
192
210
|
...(declared.surfaces ? { surfaces: declared.surfaces } : {}),
|
|
193
211
|
};
|
|
194
212
|
}
|
package/src/descriptor/frond.ts
CHANGED
|
@@ -112,6 +112,23 @@ export interface CollectorEntry {
|
|
|
112
112
|
filePath: string;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A discovered middleware — a class declaring `around(ctx, next)`, which runs before and
|
|
117
|
+
* after every operation in its scope.
|
|
118
|
+
*/
|
|
119
|
+
export interface MiddlewareEntry {
|
|
120
|
+
/** Class name — what `frond.config.ts` addresses to widen the scope. */
|
|
121
|
+
name: string;
|
|
122
|
+
/** The middleware class. */
|
|
123
|
+
ctor: new (...args: never[]) => unknown;
|
|
124
|
+
/** How far it reaches: its own frond's entities, or every operation in the process. */
|
|
125
|
+
scope: 'frond' | 'app';
|
|
126
|
+
/** Constructor dependency type names (from AST scan). */
|
|
127
|
+
deps: string[];
|
|
128
|
+
/** Absolute file path (for debugging). */
|
|
129
|
+
filePath: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
/** A discovered seed file (array of records or async factory). */
|
|
116
133
|
export interface SeedEntry {
|
|
117
134
|
/** Entity name this seed targets (from filename: Author.seed.ts → 'author'). */
|
|
@@ -142,6 +159,16 @@ export interface FrondDescriptor {
|
|
|
142
159
|
presenters: PresenterEntry[];
|
|
143
160
|
collectors: CollectorEntry[];
|
|
144
161
|
seeds: SeedEntry[];
|
|
162
|
+
middlewares: MiddlewareEntry[];
|
|
163
|
+
/**
|
|
164
|
+
* Brought by an EXTENSION rather than by the app — `@fougere/calls` keeping its lines,
|
|
165
|
+
* `@fougere/observability` sending them on. It is installed like any other, and it is not
|
|
166
|
+
* what the app SERVES: a report that lists it describes the instrumentation, not the
|
|
167
|
+
* domain. Set by the boot, never by a declaration.
|
|
168
|
+
*/
|
|
169
|
+
brought?: true;
|
|
170
|
+
/** The ops that finish a fact, in order — see `FrondConfig.pipes`. */
|
|
171
|
+
pipes?: Record<string, string[]>;
|
|
145
172
|
/**
|
|
146
173
|
* Per-surface entity lists from frond.config.ts (e.g. { graphql: ['Post'], rest: ['Post',
|
|
147
174
|
* 'Author'] }).
|
|
@@ -156,5 +183,9 @@ export interface FrondDescriptor {
|
|
|
156
183
|
handlerName?: string;
|
|
157
184
|
/** Method name on the resolved handler (defaults to op name). */
|
|
158
185
|
method?: string;
|
|
186
|
+
/** The GraphQL root field this op answers to — read by `adapter/graphql`. */
|
|
187
|
+
graphql?: string;
|
|
188
|
+
/** Where it answers over REST — read by `adapter/rest`, the dual of `graphql`. */
|
|
189
|
+
rest?: { method?: string; path?: string; status?: number };
|
|
159
190
|
}>;
|
|
160
191
|
}
|
package/src/descriptor/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { Fronds } from './Fronds.js';
|
|
|
8
8
|
export { cardinalityOf, type OperationContract, type OperationsMap, type Param, type TypeRef } from '../wire/operation.js';
|
|
9
9
|
export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
|
|
10
10
|
export type { Signature } from '../wire/signature.js';
|
|
11
|
-
export { emitKeyOf } from '../wire/emit.js';
|
|
11
|
+
export { emitKeyOf, awaitKeyOf } from '../wire/emit.js';
|
|
12
12
|
export { storageKeyOf } from '../storage/port.js';
|
|
13
13
|
export { getPresenterFields } from '../prefab/presenter.js';
|
|
14
14
|
export { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
|
|
@@ -38,8 +38,12 @@ export class ArgumentResolver {
|
|
|
38
38
|
args.push(val);
|
|
39
39
|
break;
|
|
40
40
|
}
|
|
41
|
-
case 'fact':
|
|
42
|
-
|
|
41
|
+
case 'fact':
|
|
42
|
+
case 'pipe': {
|
|
43
|
+
// A fact IS the payload — the whole of what happened, never a piece of it. The
|
|
44
|
+
// same holds before it is final: an op that finishes one is handed all of it,
|
|
45
|
+
// and answers all of it.
|
|
46
|
+
//
|
|
43
47
|
// Identical to `input` today, and deliberately not sharing its branch: the two
|
|
44
48
|
// agree by coincidence, not by rule, and the day `input` learns to look up a
|
|
45
49
|
// value by parameter name a subscriber would receive ONE FIELD of the fact it
|
|
@@ -13,7 +13,7 @@ import { repositoryKeyOf } from '../prefab/repository.js';
|
|
|
13
13
|
import type { OperationContract, OperationsMap } from '../wire/operation.js';
|
|
14
14
|
import type { EffectiveOperation, EffectiveOperationsMap } from '../effective-operation.js';
|
|
15
15
|
import type { BindingPlan } from '../wire/binding.js';
|
|
16
|
-
import {
|
|
16
|
+
import { Invocation, type InvocationContext } from '../wire/Invocation.js';
|
|
17
17
|
import type { HandlerEntry, PresenterEntry } from '../descriptor/frond.js';
|
|
18
18
|
import { ArgumentResolver } from './ArgumentResolver.js';
|
|
19
19
|
import { OutputView } from './OutputView.js';
|
|
@@ -101,7 +101,7 @@ export class HandlerFacade {
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const invocation =
|
|
104
|
+
const invocation = Invocation.from(input);
|
|
105
105
|
const context: OperationContext = {
|
|
106
106
|
entity,
|
|
107
107
|
frond: this.door.frond,
|
|
@@ -229,13 +229,19 @@ export function resolveEffectiveOperations(
|
|
|
229
229
|
|
|
230
230
|
// A fact is validated by the entity it names. This used to be patched into the
|
|
231
231
|
// facade after resolution, leaving check/explain with a different input.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
232
|
+
const announced = contract.binding.find((binding) =>
|
|
233
|
+
binding.source.kind === 'fact' || binding.source.kind === 'pipe');
|
|
234
|
+
const fact = announced?.source.kind === 'fact' || announced?.source.kind === 'pipe'
|
|
235
|
+
? schemas.get(announced.source.factName)
|
|
236
|
+
: undefined;
|
|
237
|
+
const input = contract.input ?? fact;
|
|
238
|
+
|
|
239
|
+
// An op that FINISHES a fact answers the fact, so its answer is the fact's shape:
|
|
240
|
+
// derived here, where its input already is. Left to the ordinary projection it
|
|
241
|
+
// came back stripped — measured, the subscriber got a value with no `id`.
|
|
242
|
+
const output = announced?.source.kind === 'pipe' && fact
|
|
243
|
+
? { schema: fact, closed: false }
|
|
244
|
+
: effectiveOutput(frond, handler, name, contract);
|
|
239
245
|
const className = handler.ctor.name.endsWith('Handler')
|
|
240
246
|
? handler.ctor.name.slice(0, -'Handler'.length)
|
|
241
247
|
: handler.ctor.name;
|
package/src/frond-config.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface OperationOverride {
|
|
|
11
11
|
kind?: 'query' | 'command';
|
|
12
12
|
/** The GraphQL root field this op answers to. */
|
|
13
13
|
graphql?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Where this op answers over REST — the dual of `graphql:`, and the frond's to say for
|
|
16
|
+
* the same reason: how an operation is CALLED belongs beside it, not in whatever host
|
|
17
|
+
* mounts the router. The prefix stays the host's: naming is the frond's, mounting is not.
|
|
18
|
+
*/
|
|
19
|
+
rest?: { method?: string; path?: string; status?: number };
|
|
14
20
|
/**
|
|
15
21
|
* Handler class to delegate to (overrides the default `{Entity}Handler` lookup).
|
|
16
22
|
* Class name is used to resolve from DI. E.g. `ArchiveHandler` → `app.resolve('ArchiveHandler')`.
|
|
@@ -44,6 +50,20 @@ export interface FrondConfig {
|
|
|
44
50
|
surfaces?: Record<string, string[]>;
|
|
45
51
|
/** Interface → implementation bindings for DI (e.g. { Database: 'SqliteDatabase' }). */
|
|
46
52
|
bindings?: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* The ops that FINISH a fact, in the order they run — by class name, keyed by fact.
|
|
55
|
+
*
|
|
56
|
+
* Declared by the frond that OWNS the fact, because ordering is a decision about the
|
|
57
|
+
* fact itself and it has one owner. Without it two links refuse: nothing would say which
|
|
58
|
+
* ran first, and scan order is not an answer.
|
|
59
|
+
*/
|
|
60
|
+
pipes?: Record<string, string[]>;
|
|
61
|
+
/**
|
|
62
|
+
* How far a middleware reaches, by class name. A middleware answers for its own frond
|
|
63
|
+
* without being named here; `'app'` is the exception, and it is stated by the frond
|
|
64
|
+
* that decides for the others.
|
|
65
|
+
*/
|
|
66
|
+
middlewares?: Record<string, 'frond' | 'app'>;
|
|
47
67
|
/** Per-operation overrides. Key = operation name (method name on a handler). */
|
|
48
68
|
operations?: Record<string, OperationOverride>;
|
|
49
69
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,87 +4,74 @@ export { orderSeeds, runSeeds, seeding } from './boot/seed.js';
|
|
|
4
4
|
export { AppLifecycle, migrating } from './boot/AppLifecycle.js';
|
|
5
5
|
export type { Extension } from './boot/AppLifecycle.js';
|
|
6
6
|
export { defineFougere } from './define.js';
|
|
7
|
-
export type {
|
|
8
|
-
export type {
|
|
7
|
+
export type { FougereConfig, AdapterConfig } from './config-loader.js';
|
|
8
|
+
export type { OperationOverride, FrondConfig } from './frond-config.js';
|
|
9
9
|
export { buildGraph, clusterEntities, suggestSplit, type EntityNode, type DomainCluster } from './graph.js';
|
|
10
10
|
export { Crud } from './prefab/crud.js';
|
|
11
|
-
export type { CrudOps, CrudOpName,
|
|
11
|
+
export type { CrudViews, CrudOps, CrudOpName, CrudConstructor } from './prefab/crud.js';
|
|
12
12
|
export { Mirror } from './prefab/mirror.js';
|
|
13
|
-
export type { MirrorOf, MirrorConstructor
|
|
13
|
+
export type { Refreshed, MirrorOf, MirrorConstructor } from './prefab/mirror.js';
|
|
14
14
|
export { Presenter } from './prefab/presenter.js';
|
|
15
15
|
export type { PresenterViews } from './prefab/presenter.js';
|
|
16
16
|
export { Collector } from './prefab/collector.js';
|
|
17
17
|
export { Repository, type RepositoryOf, type RepositoryConstructor, type AggregateOf, type AggregateConstructor } from './prefab/repository.js';
|
|
18
|
-
export { Invocation
|
|
19
|
-
export type { InvocationContext
|
|
20
|
-
export { Call } from './
|
|
21
|
-
export { RouteAddress } from './
|
|
22
|
-
export type { RouteAddressInput } from './wire/RouteAddress.js';
|
|
18
|
+
export { Invocation } from './contract.js';
|
|
19
|
+
export type { InvocationContext } from './contract.js';
|
|
20
|
+
export { Call } from './contract.js';
|
|
21
|
+
export { RouteAddress } from './contract.js';
|
|
23
22
|
export { DispatchLifecycle } from './dispatch/DispatchLifecycle.js';
|
|
24
23
|
export { Dispatcher } from './dispatch/Dispatcher.js';
|
|
25
24
|
// The class and not only its type: an observer RECEIVES these, so a package that folds
|
|
26
25
|
// them — or a test of one — has to be able to make one through the door.
|
|
27
26
|
export { DispatchEvent } from './dispatch/DispatchEvent.js';
|
|
28
|
-
export type { CallPage, CallRecord } from './
|
|
29
|
-
export { driftOf, agrees, explain, type CardDrift } from './
|
|
30
|
-
export type { DispatchObserver } from './dispatch/DispatchEvent.js';
|
|
31
|
-
export type { DispatchPort } from './dispatch/DispatchPort.js';
|
|
32
|
-
export {
|
|
33
|
-
inferOperationKind,
|
|
34
|
-
resolveIsReadOp,
|
|
35
|
-
resolveOperationKind,
|
|
36
|
-
} from './wire/operation.js';
|
|
37
|
-
export { resolveContracts } from './effective-operation.js';
|
|
27
|
+
export type { CallPage, CallRecord } from './contract.js';
|
|
28
|
+
export { driftOf, agrees, explain, type CardDrift } from './contract.js';
|
|
38
29
|
export type {
|
|
39
30
|
OperationContract,
|
|
40
|
-
OperationKind,
|
|
41
|
-
OperationKindInference,
|
|
42
31
|
OperationsMap,
|
|
43
32
|
Param,
|
|
44
33
|
TypeRef,
|
|
45
34
|
} from './wire/operation.js';
|
|
46
|
-
export {
|
|
47
|
-
|
|
48
|
-
EffectiveOperationModel,
|
|
49
|
-
resolveEffectiveOperations,
|
|
50
|
-
} from './effective-operation.js';
|
|
51
|
-
export type {
|
|
52
|
-
EffectiveCollector,
|
|
53
|
-
EffectiveOperation,
|
|
54
|
-
EffectiveOperationOptions,
|
|
55
|
-
EffectiveOperationsMap,
|
|
56
|
-
EffectiveParameter,
|
|
57
|
-
} from './effective-operation.js';
|
|
35
|
+
export { resolveEffectiveOperations } from './effective-operation.js';
|
|
36
|
+
export type { EffectiveOperation } from './effective-operation.js';
|
|
58
37
|
export type { ModuleLoader } from './loader.js';
|
|
59
|
-
export { FougereError, ErrorCode, validationErrorsOf
|
|
60
|
-
export type { OperationContext, AppNext
|
|
61
|
-
export { assertIdentityCard, RPC_ENTITY, MAX_BODY_BYTES } from './
|
|
38
|
+
export { FougereError, ErrorCode, validationErrorsOf } from './contract.js';
|
|
39
|
+
export type { AppMiddleware, OperationContext, AppNext } from './wire/middleware.js';
|
|
40
|
+
export { assertIdentityCard, RPC_ENTITY, MAX_BODY_BYTES } from './contract.js';
|
|
62
41
|
export { createLocalRunner, createAppRunner } from './boot/runner.js';
|
|
63
42
|
|
|
64
43
|
// A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
|
|
65
44
|
// is why it sits here and not in `/node`, beside the scanner that reads a project.
|
|
66
45
|
export { frond } from './declare.js';
|
|
67
|
-
export type {
|
|
46
|
+
export type { Declared, FrondDeclaration, DeclaredSubject } from './declare.js';
|
|
68
47
|
// What this app would answer to `rpc.discover`. Served by the boot, and reached until now
|
|
69
48
|
// only by tests through a deep path — a consumer comparing its own card to a producer's
|
|
70
49
|
// had to dispatch a call to ask a question about itself.
|
|
71
50
|
export { identityCardOf } from './boot/card.js';
|
|
51
|
+
export type { Facade } from './wire/call.js';
|
|
72
52
|
export type {
|
|
73
|
-
FrondCall,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
53
|
+
FrondCall,
|
|
54
|
+
Transport,
|
|
55
|
+
IdentityCard,
|
|
56
|
+
CardOp,
|
|
57
|
+
TopologyReport,
|
|
58
|
+
FrondPlacement,
|
|
59
|
+
Edge,
|
|
60
|
+
} from './contract.js';
|
|
61
|
+
export { type Emit, type Fact, type Pipe } from './wire/emit.js';
|
|
77
62
|
export { callValueOf } from './contract.js';
|
|
78
|
-
export
|
|
79
|
-
export {
|
|
63
|
+
export { toHttpError } from './wire/http-error.js';
|
|
64
|
+
export { toPublicError } from './contract.js';
|
|
80
65
|
export { loggerMiddleware } from './wire/loggerMiddleware.js';
|
|
81
|
-
export { Logger, setLogLevel, logLevel, envLevel, onLog } from './builtin/logger.js';
|
|
82
|
-
export type {
|
|
66
|
+
export { Carry, Logger, setLogLevel, logLevel, envLevel, onLog, formatted } from './builtin/logger.js';
|
|
67
|
+
export type { LogRecord, Rendered, LogSink } from './builtin/logger.js';
|
|
68
|
+
// The fact a boot announces. Its destinations are `@fougere/log`'s; the line is core's.
|
|
69
|
+
export { default as LogLine, CARRIES_LINE, LOG_LINE } from './builtin/LogLine.js';
|
|
83
70
|
// What a re-read config changes in a running process — and what it cannot.
|
|
84
|
-
export { applyConfig
|
|
71
|
+
export { applyConfig } from './boot/apply.js';
|
|
85
72
|
export { Config } from './builtin/config.js';
|
|
86
73
|
export type { Storage, StorageFactory, ListOptions, ListResult, Together } from './storage/port.js';
|
|
87
|
-
export { togetherKeyOf
|
|
74
|
+
export { togetherKeyOf } from './storage/port.js';
|
|
88
75
|
export type { Constraint, Source, SourceConfig, SourceView } from './source.js';
|
|
89
76
|
export { Sources } from './source.js';
|
|
90
77
|
export { storageOver } from './storage/store.js';
|
|
@@ -102,17 +89,16 @@ export type {
|
|
|
102
89
|
EntityEntry,
|
|
103
90
|
HandlerEntry,
|
|
104
91
|
SeedEntry,
|
|
105
|
-
SeedFactory,
|
|
106
92
|
PresenterEntry,
|
|
107
93
|
PresenterFieldMeta,
|
|
108
94
|
CollectorEntry,
|
|
109
95
|
} from './descriptor/frond.js';
|
|
110
96
|
export type { AuthConfig, AuthContext, AuthRuntime } from './boot/auth.js';
|
|
111
97
|
export { Fronds } from './descriptor/Fronds.js';
|
|
112
|
-
export { verify,
|
|
98
|
+
export { verify, type Violation } from './verify.js';
|
|
113
99
|
// Same question as verify(), answered from the source text instead of the model.
|
|
114
100
|
// Who is calling, established rather than accepted — the proof beside `state`'s claim.
|
|
115
101
|
// `generateKeyPair` and `issueGrant` are NOT here: they make keys at a deployment and
|
|
116
102
|
// need `node:crypto` for a gesture WebCrypto has no equal of. They sit on `/node`.
|
|
117
103
|
export { signEnvelope, verifyEnvelope, identityFromEnv } from './identity.js';
|
|
118
|
-
export type { FrondIdentity,
|
|
104
|
+
export type { FrondIdentity, SignedCall } from './identity.js';
|
package/src/wire/Invocation.ts
CHANGED
|
@@ -60,6 +60,8 @@ export class Invocation implements InvocationContext {
|
|
|
60
60
|
return context instanceof Invocation ? context : new Invocation(context ?? {});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
static readonly empty = Invocation.from();
|
|
64
|
+
|
|
63
65
|
/** Replaces the input — how the façade hands on the value it parsed. */
|
|
64
66
|
withInput(input: unknown): Invocation {
|
|
65
67
|
return new Invocation({ ...this, input });
|
|
@@ -70,10 +72,3 @@ export class Invocation implements InvocationContext {
|
|
|
70
72
|
return new Invocation({ ...this, state });
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
export const EMPTY_INVOCATION = Invocation.from();
|
|
75
|
-
|
|
76
|
-
/** Names the same gesture as `Invocation.from` for callers that read better this way. */
|
|
77
|
-
export function canonicalInvocation(context?: PartialInvocation): Invocation {
|
|
78
|
-
return Invocation.from(context);
|
|
79
|
-
}
|