@fougere/core 0.5.0-alpha.0 → 0.6.0-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/boot/Emissions.d.ts +2 -2
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +1 -1
- package/dist/boot/HandlerFacade.d.ts +1 -1
- package/dist/boot/HandlerFacade.d.ts.map +1 -1
- package/dist/boot/HandlerFacade.js +1 -1
- package/dist/boot/HandlerFacade.js.map +1 -1
- package/dist/boot/auth.d.ts +6 -6
- package/dist/boot/auth.d.ts.map +1 -1
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/boot.d.ts.map +1 -1
- package/dist/boot/boot.js +1 -1
- package/dist/boot/boot.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +47 -38
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts +21 -0
- package/dist/boot/card.d.ts.map +1 -0
- package/dist/boot/card.js +100 -0
- package/dist/boot/card.js.map +1 -0
- package/dist/boot/frame.d.ts +3 -3
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +7 -7
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/hosted.d.ts +8 -2
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +10 -10
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/ownership.d.ts +7 -5
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +9 -14
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/ports.d.ts +2 -2
- package/dist/boot/ports.d.ts.map +1 -1
- package/dist/boot/ports.js +1 -1
- package/dist/boot/runner.d.ts +28 -0
- package/dist/boot/runner.d.ts.map +1 -0
- package/dist/boot/runner.js +26 -0
- package/dist/boot/runner.js.map +1 -0
- package/dist/boot/seed.d.ts +2 -2
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +5 -5
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +2 -1
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts +4 -4
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +17 -19
- package/dist/boot/together.js.map +1 -1
- package/dist/boot/types.d.ts +12 -11
- package/dist/boot/types.d.ts.map +1 -1
- package/dist/contract/CallLog.d.ts +2 -2
- package/dist/contract/CallLog.d.ts.map +1 -1
- package/dist/contract/RouteAddress.d.ts +2 -0
- package/dist/contract/RouteAddress.d.ts.map +1 -1
- package/dist/contract/RouteAddress.js.map +1 -1
- package/dist/{scan/declare.d.ts → declare.d.ts} +3 -3
- package/dist/declare.d.ts.map +1 -0
- package/dist/{scan/declare.js → declare.js} +3 -3
- package/dist/declare.js.map +1 -0
- package/dist/{scan → descriptor}/Fronds.d.ts +6 -7
- package/dist/descriptor/Fronds.d.ts.map +1 -0
- package/dist/{scan → descriptor}/Fronds.js +6 -7
- package/dist/descriptor/Fronds.js.map +1 -0
- package/dist/{scan → descriptor}/frond.d.ts +7 -47
- package/dist/descriptor/frond.d.ts.map +1 -0
- package/dist/{scan → descriptor}/frond.js.map +1 -1
- package/dist/dispatch/ArgumentResolver.d.ts +5 -2
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/ArgumentResolver.js +48 -3
- package/dist/dispatch/ArgumentResolver.js.map +1 -1
- package/dist/dispatch/DispatchEvent.d.ts +1 -1
- package/dist/dispatch/DispatchEvent.d.ts.map +1 -1
- package/dist/dispatch/PresenterArgumentResolver.d.ts +1 -1
- package/dist/dispatch/PresenterArgumentResolver.d.ts.map +1 -1
- package/dist/dispatch/PresenterArgumentResolver.js +1 -1
- package/dist/dispatch/Route.d.ts +1 -2
- package/dist/dispatch/Route.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts +2 -2
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +7 -7
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/effective-operation.d.ts +29 -3
- package/dist/effective-operation.d.ts.map +1 -1
- package/dist/effective-operation.js +52 -1
- package/dist/effective-operation.js.map +1 -1
- package/dist/emit.d.ts +1 -1
- package/dist/frond-config.d.ts +2 -2
- package/dist/graph.d.ts +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/imports.d.ts +1 -1
- package/dist/imports.d.ts.map +1 -1
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -2
- package/dist/node.js.map +1 -1
- package/dist/prefab/collector.d.ts +2 -2
- package/dist/prefab/collector.js +2 -2
- package/dist/prefab/crud.d.ts +5 -5
- package/dist/prefab/crud.d.ts.map +1 -1
- package/dist/prefab/crud.js +12 -12
- package/dist/prefab/crud.js.map +1 -1
- package/dist/prefab/mirror.d.ts +5 -5
- package/dist/prefab/mirror.d.ts.map +1 -1
- package/dist/prefab/mirror.js +5 -5
- package/dist/prefab/mirror.js.map +1 -1
- package/dist/prefab/presenter.d.ts +2 -2
- package/dist/prefab/presenter.js +2 -2
- package/dist/prefab/repository.d.ts +14 -14
- package/dist/prefab/repository.d.ts.map +1 -1
- package/dist/prefab/repository.js +19 -19
- package/dist/prefab/repository.js.map +1 -1
- package/dist/rows.d.ts +40 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +146 -0
- package/dist/rows.js.map +1 -0
- package/dist/scan/conventions.d.ts +8 -12
- package/dist/scan/conventions.d.ts.map +1 -1
- package/dist/scan/conventions.js +0 -13
- package/dist/scan/conventions.js.map +1 -1
- package/dist/scan/emit.d.ts +1 -1
- package/dist/scan/emit.d.ts.map +1 -1
- package/dist/scan/emit.js +1 -1
- package/dist/scan/emit.js.map +1 -1
- package/dist/scan/handler-parser.d.ts +11 -56
- package/dist/scan/handler-parser.d.ts.map +1 -1
- package/dist/scan/handler-parser.js +2 -2
- package/dist/scan/handler-parser.js.map +1 -1
- package/dist/scan/result.d.ts +41 -0
- package/dist/scan/result.d.ts.map +1 -0
- package/dist/scan/result.js +2 -0
- package/dist/scan/result.js.map +1 -0
- package/dist/scan/scanner.d.ts +18 -1
- package/dist/scan/scanner.d.ts.map +1 -1
- package/dist/scan/scanner.js +34 -17
- package/dist/scan/scanner.js.map +1 -1
- package/dist/scan/statement.d.ts +1 -1
- package/dist/scan/statement.d.ts.map +1 -1
- package/dist/scan/statement.js.map +1 -1
- package/dist/{orm.d.ts → storage.d.ts} +98 -16
- package/dist/storage.d.ts.map +1 -0
- package/dist/{orm.js → storage.js} +53 -5
- package/dist/storage.js.map +1 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +2 -2
- package/dist/verify.js.map +1 -1
- package/dist/{boot → wire}/binding.d.ts +2 -10
- package/dist/wire/binding.d.ts.map +1 -0
- package/dist/{boot → wire}/binding.js +0 -52
- package/dist/wire/binding.js.map +1 -0
- package/dist/wire/call.d.ts +2 -40
- package/dist/wire/call.d.ts.map +1 -1
- package/dist/wire/call.js +0 -125
- package/dist/wire/call.js.map +1 -1
- package/dist/wire/operation.d.ts +5 -31
- package/dist/wire/operation.d.ts.map +1 -1
- package/dist/wire/operation.js +0 -51
- package/dist/wire/operation.js.map +1 -1
- package/dist/wire/signature.d.ts +58 -0
- package/dist/wire/signature.d.ts.map +1 -0
- package/dist/wire/signature.js +2 -0
- package/dist/wire/signature.js.map +1 -0
- package/package.json +3 -3
- package/src/boot/Emissions.ts +2 -2
- package/src/boot/HandlerFacade.ts +3 -3
- package/src/boot/auth.ts +6 -6
- package/src/boot/boot.ts +3 -3
- package/src/boot/bootstrap.ts +50 -39
- package/src/boot/card.ts +113 -0
- package/src/boot/frame.ts +7 -7
- package/src/boot/hosted.ts +11 -14
- package/src/boot/ownership.ts +14 -15
- package/src/boot/ports.ts +2 -2
- package/src/boot/runner.ts +34 -0
- package/src/boot/seed.ts +6 -6
- package/src/boot/statement-drift.ts +5 -4
- package/src/boot/together.ts +24 -25
- package/src/boot/types.ts +12 -11
- package/src/contract/CallLog.ts +2 -2
- package/src/contract/RouteAddress.ts +3 -0
- package/src/{scan/declare.ts → declare.ts} +5 -5
- package/src/{scan → descriptor}/Fronds.ts +6 -7
- package/src/{scan → descriptor}/frond.ts +7 -48
- package/src/dispatch/ArgumentResolver.ts +53 -7
- package/src/dispatch/DispatchEvent.ts +1 -1
- package/src/dispatch/PresenterArgumentResolver.ts +2 -2
- package/src/dispatch/Route.ts +1 -3
- package/src/dispatch/StorageGuard.ts +7 -7
- package/src/effective-operation.ts +63 -11
- package/src/emit.ts +1 -1
- package/src/frond-config.ts +2 -2
- package/src/graph.ts +1 -1
- package/src/imports.ts +1 -1
- package/src/index.ts +15 -13
- package/src/node.ts +2 -2
- package/src/prefab/collector.ts +2 -2
- package/src/prefab/crud.ts +18 -18
- package/src/prefab/mirror.ts +8 -8
- package/src/prefab/presenter.ts +2 -2
- package/src/prefab/repository.ts +32 -32
- package/src/rows.ts +155 -0
- package/src/scan/conventions.ts +0 -18
- package/src/scan/emit.ts +3 -5
- package/src/scan/handler-parser.ts +14 -67
- package/src/scan/result.ts +42 -0
- package/src/scan/scanner.ts +44 -21
- package/src/scan/statement.ts +2 -1
- package/src/{orm.ts → storage.ts} +126 -17
- package/src/verify.ts +3 -3
- package/src/{boot → wire}/binding.ts +2 -66
- package/src/wire/call.ts +1 -129
- package/src/wire/operation.ts +5 -62
- package/src/wire/signature.ts +59 -0
- package/dist/boot/binding.d.ts.map +0 -1
- package/dist/boot/binding.js.map +0 -1
- package/dist/orm.d.ts.map +0 -1
- package/dist/orm.js.map +0 -1
- package/dist/scan/Fronds.d.ts.map +0 -1
- package/dist/scan/Fronds.js.map +0 -1
- package/dist/scan/declare.d.ts.map +0 -1
- package/dist/scan/declare.js.map +0 -1
- package/dist/scan/frond.d.ts.map +0 -1
- /package/dist/{scan → descriptor}/frond.js +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* The AST says what the author declared; TypeScript's checker says what those types mean.
|
|
5
5
|
* TypeScript is lazy-loaded to avoid bundling the compiler in production builds.
|
|
6
6
|
*/
|
|
7
|
+
import type { TypeRef, Param, Signature } from '../wire/signature.js';
|
|
7
8
|
import type ts from '@typescript/typescript6';
|
|
8
9
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
9
10
|
import { join, dirname, resolve as resolvePath } from 'node:path';
|
|
@@ -195,7 +196,7 @@ function sourceOf(filePath: string): ts.SourceFile {
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
/** One declared parameter — a constructor's and a method's are read the same way. */
|
|
198
|
-
function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, checker?: ts.TypeChecker):
|
|
199
|
+
function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, checker?: ts.TypeChecker): Param {
|
|
199
200
|
const ts = getTS();
|
|
200
201
|
const type = param.type ? parseTypeNode(param.type, source, checker) : { raw: 'unknown', name: 'unknown' };
|
|
201
202
|
return {
|
|
@@ -207,63 +208,9 @@ function parsedParam(param: ts.ParameterDeclaration, source: ts.SourceFile, chec
|
|
|
207
208
|
};
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
/** A parsed type reference — supports primitives, entities, arrays, generics. */
|
|
211
|
-
export interface ParsedType {
|
|
212
|
-
/** Raw type text as written in source (e.g. 'Pagination<Post>'). */
|
|
213
|
-
raw: string;
|
|
214
|
-
/** Base type name (e.g. 'Pagination', 'string', 'Post'). */
|
|
215
|
-
name: string;
|
|
216
|
-
/** Whether this is an array (T[] or Array<T>) — true at any depth. */
|
|
217
|
-
array?: boolean;
|
|
218
|
-
/**
|
|
219
|
-
* How MANY array levels: `string[]` is 1, `string[][]` is 2. `array` only ever said
|
|
220
|
-
* "at least one", which sufficed while one level meant one thing. It stopped sufficing
|
|
221
|
-
* when a presenter method started taking the page — there the outer level IS the page
|
|
222
|
-
* and what remains is the field, so telling `string[]` from `string[][]` is telling a
|
|
223
|
-
* computed string from a computed list.
|
|
224
|
-
*/
|
|
225
|
-
arrayDepth?: number;
|
|
226
|
-
/** Generic type arguments (e.g. for Pagination<Post> → [{ name: 'Post' }]). */
|
|
227
|
-
generics?: ParsedType[];
|
|
228
|
-
/** Whether `null` belongs to the type. */
|
|
229
|
-
nullable?: boolean;
|
|
230
|
-
/** Whether absence (`undefined` or `void`) belongs to the type. */
|
|
231
|
-
undefined?: boolean;
|
|
232
|
-
/** Whether this is a Promise wrapper (unwrapped in output). */
|
|
233
|
-
promise?: boolean;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** A parsed method parameter. */
|
|
237
|
-
export interface ParsedParam {
|
|
238
|
-
name: string;
|
|
239
|
-
type: ParsedType;
|
|
240
|
-
optional?: boolean;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** A parsed method signature from a handler source file. */
|
|
244
|
-
export interface ParsedMethod {
|
|
245
|
-
name: string;
|
|
246
|
-
params: ParsedParam[];
|
|
247
|
-
returnType?: ParsedType;
|
|
248
|
-
/**
|
|
249
|
-
* Came from a base class, not from the file being scanned. What a prefab
|
|
250
|
-
* handler declares about its own ops beats this — the scan reads a signature
|
|
251
|
-
* and guesses, the builder knows.
|
|
252
|
-
*/
|
|
253
|
-
inherited?: boolean;
|
|
254
|
-
/**
|
|
255
|
-
* The operation in words — the first sentence of the method's doc comment.
|
|
256
|
-
*
|
|
257
|
-
* Not a new thing to write: handlers already carry it (`/** Judge: the author,
|
|
258
|
-
* a draft… */`), the AST already holds it, and nothing read it. A caller that
|
|
259
|
-
* discovers an operation over the wire has its name and its schema; what the
|
|
260
|
-
* operation is FOR lived only in the source.
|
|
261
|
-
*/
|
|
262
|
-
description?: string;
|
|
263
|
-
}
|
|
264
211
|
|
|
265
|
-
/** Parse a TypeScript type node into a
|
|
266
|
-
function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.TypeChecker):
|
|
212
|
+
/** Parse a TypeScript type node into a TypeRef. */
|
|
213
|
+
function parseTypeNode(node: ts.TypeNode, source: ts.SourceFile, checker?: ts.TypeChecker): TypeRef {
|
|
267
214
|
const ts = getTS();
|
|
268
215
|
const raw = node.getText(source);
|
|
269
216
|
|
|
@@ -373,7 +320,7 @@ function meaningfulSymbolName(type: ts.Type, checker: ts.TypeChecker): string {
|
|
|
373
320
|
}
|
|
374
321
|
|
|
375
322
|
/** Turn a checked TypeScript type into the small, serializable vocabulary the runtime reads. */
|
|
376
|
-
function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, depth = 0):
|
|
323
|
+
function parseCheckedType(type: ts.Type, raw: string, checker: ts.TypeChecker, depth = 0): TypeRef {
|
|
377
324
|
const typescript = getTS();
|
|
378
325
|
if (depth > 12) return { raw, name: checker.typeToString(type) };
|
|
379
326
|
|
|
@@ -545,9 +492,9 @@ function extractClassMethods(
|
|
|
545
492
|
source: ts.SourceFile,
|
|
546
493
|
skip: Set<string>,
|
|
547
494
|
checker?: ts.TypeChecker,
|
|
548
|
-
):
|
|
495
|
+
): Signature[] {
|
|
549
496
|
const ts = getTS();
|
|
550
|
-
const results:
|
|
497
|
+
const results: Signature[] = [];
|
|
551
498
|
|
|
552
499
|
for (const member of cls.members) {
|
|
553
500
|
if (!ts.isMethodDeclaration(member)) continue;
|
|
@@ -628,9 +575,9 @@ function inheritedFromBase(
|
|
|
628
575
|
base: ts.ExpressionWithTypeArguments,
|
|
629
576
|
checker: ts.TypeChecker,
|
|
630
577
|
skip: Set<string>,
|
|
631
|
-
):
|
|
578
|
+
): Signature[] {
|
|
632
579
|
const typescript = getTS();
|
|
633
|
-
const results:
|
|
580
|
+
const results: Signature[] = [];
|
|
634
581
|
|
|
635
582
|
for (const property of checker.getPropertiesOfType(checker.getTypeAtLocation(base))) {
|
|
636
583
|
if (skip.has(property.name)) continue;
|
|
@@ -676,7 +623,7 @@ function parseInheritedMethods(
|
|
|
676
623
|
* published base class was absent from the façade without a word.
|
|
677
624
|
*/
|
|
678
625
|
unresolved: string[],
|
|
679
|
-
):
|
|
626
|
+
): Signature[] {
|
|
680
627
|
const ts = getTS();
|
|
681
628
|
if (!cls.heritageClauses) return [];
|
|
682
629
|
|
|
@@ -747,7 +694,7 @@ function findDefaultClass(source: ts.SourceFile): ts.ClassDeclaration | undefine
|
|
|
747
694
|
* pair travels through the scan cache, which is why {@link PARSER_VERSION} moved.
|
|
748
695
|
*/
|
|
749
696
|
export interface HandlerParse {
|
|
750
|
-
methods:
|
|
697
|
+
methods: Signature[];
|
|
751
698
|
/** Base classes whose source this pass could not open. Empty is a claim. */
|
|
752
699
|
unresolvedHeritage: string[];
|
|
753
700
|
}
|
|
@@ -766,7 +713,7 @@ export async function parseAllHandlerMethods(filePath: string, projectRoot?: str
|
|
|
766
713
|
*
|
|
767
714
|
* Returns all methods (no CRUD filtering) — each method is a computed field.
|
|
768
715
|
*/
|
|
769
|
-
export async function parsePresenterMethods(filePath: string, projectRoot?: string): Promise<
|
|
716
|
+
export async function parsePresenterMethods(filePath: string, projectRoot?: string): Promise<Signature[]> {
|
|
770
717
|
await loadTS();
|
|
771
718
|
// No `projectRoot`, so no heritage pass and nothing to report: a presenter's
|
|
772
719
|
// computed fields are its own methods.
|
|
@@ -775,9 +722,9 @@ export async function parsePresenterMethods(filePath: string, projectRoot?: stri
|
|
|
775
722
|
|
|
776
723
|
/**
|
|
777
724
|
* Parse constructor parameter types from a source file's default class.
|
|
778
|
-
* Returns type names (e.g. ['
|
|
725
|
+
* Returns type names (e.g. ['PostStorage', 'Logger']) for DI resolution.
|
|
779
726
|
*/
|
|
780
|
-
export async function parseConstructorParams(filePath: string, projectRoot?: string): Promise<
|
|
727
|
+
export async function parseConstructorParams(filePath: string, projectRoot?: string): Promise<Param[]> {
|
|
781
728
|
const ts = await loadTS();
|
|
782
729
|
const { source, checker } = checkedSourceOf(filePath, projectRoot);
|
|
783
730
|
const cls = findDefaultClass(source);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Fronds } from '../descriptor/Fronds.js';
|
|
2
|
+
|
|
3
|
+
/** Result of scanning a project directory. */
|
|
4
|
+
/**
|
|
5
|
+
* Something the scan could NOT do — recorded instead of swallowed.
|
|
6
|
+
*
|
|
7
|
+
* The scan answers with what it found. Until now it answered the same way whether
|
|
8
|
+
* a directory held nothing or could not be read, and whether a handler declared no
|
|
9
|
+
* operation or failed to parse: `catch → empty`. So every downstream reader — the
|
|
10
|
+
* façade, the identity card, anything asking "what does this app serve?" — could
|
|
11
|
+
* not tell **"there is nothing"** from **"I could not look"**.
|
|
12
|
+
*
|
|
13
|
+
* That distinction is what makes a rule about an ABSENCE sound. Without it, a check
|
|
14
|
+
* derived from the scan reports "nothing wrong" precisely when it read nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface ScanDiagnostic {
|
|
17
|
+
/**
|
|
18
|
+
* `blocking` — the app now serves less than its source declares, and no caller
|
|
19
|
+
* can know it: a handler that failed to parse contributes zero operations.
|
|
20
|
+
* `warning` — something may be missing and the scan cannot decide, e.g. a base
|
|
21
|
+
* class it is not allowed to resolve. Statable in `frond.config.ts`.
|
|
22
|
+
*/
|
|
23
|
+
severity: 'blocking' | 'warning';
|
|
24
|
+
/** Stable rule name — `handler-parse-failed`, `directory-unreadable`. */
|
|
25
|
+
code: string;
|
|
26
|
+
/** Absolute path of what could not be read. */
|
|
27
|
+
filePath: string;
|
|
28
|
+
/** The frond it belongs to, when the scan got far enough to know. */
|
|
29
|
+
frond?: string;
|
|
30
|
+
/** The declaration the diagnostic is about — e.g. `PostHandler.publish`. */
|
|
31
|
+
subject?: string;
|
|
32
|
+
/** What could not be done, and what it costs. One sentence, for a human. */
|
|
33
|
+
message: string;
|
|
34
|
+
/** The underlying failure, kept whole. */
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScanResult {
|
|
39
|
+
fronds: Fronds;
|
|
40
|
+
/** What the scan could not do. Empty is a claim, not a default — see {@link ScanDiagnostic}. */
|
|
41
|
+
diagnostics: ScanDiagnostic[];
|
|
42
|
+
}
|
package/src/scan/scanner.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync, type Dirent } from 'node:fs';
|
|
3
3
|
import { join, dirname, basename, resolve as resolvePath } from 'node:path';
|
|
4
|
-
import type { FrondDescriptor, ProviderEntry, EntityEntry, HandlerEntry, PresenterEntry, CollectorEntry, SeedEntry
|
|
4
|
+
import type { FrondDescriptor, ProviderEntry, EntityEntry, HandlerEntry, PresenterEntry, CollectorEntry, SeedEntry } from '../descriptor/frond.js';
|
|
5
|
+
import type { ScanResult, ScanDiagnostic } from './result.js';
|
|
5
6
|
import { ANONYMOUS_SCHEMA_NAME, type SchemaView } from '@fougere/schema';
|
|
6
7
|
import type { OperationContract, OperationsMap } from '../wire/operation.js';
|
|
7
8
|
import { cardinalityOf } from '../wire/operation.js';
|
|
8
|
-
import { computeBindingPlan } from '../
|
|
9
|
+
import { computeBindingPlan } from '../wire/binding.js';
|
|
9
10
|
import {
|
|
10
11
|
parseAllHandlerMethods,
|
|
11
12
|
parsePresenterMethods,
|
|
12
13
|
parseConstructorParams,
|
|
13
14
|
resetTypePrograms,
|
|
14
15
|
seedTypeProgram,
|
|
15
|
-
type ParsedType,
|
|
16
16
|
} from './handler-parser.js';
|
|
17
|
+
import type { TypeRef } from '../wire/signature.js';
|
|
17
18
|
import { loadFrondConfig } from '../frond-config.js';
|
|
18
19
|
import { emitKeyOf } from '../emit.js';
|
|
19
20
|
import { getPresenterFields } from '../prefab/presenter.js';
|
|
20
|
-
import {
|
|
21
|
+
import { storageKeyOf, togetherKeyOf } from '../storage.js';
|
|
21
22
|
import { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
|
|
22
23
|
import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
|
|
23
24
|
import { lowerFirst } from '@fougere/schema';
|
|
24
|
-
import { Fronds } from '
|
|
25
|
+
import { Fronds } from '../descriptor/Fronds.js';
|
|
25
26
|
import { getModuleLoader } from '../loader.js';
|
|
26
27
|
import {
|
|
27
28
|
type Conventions, type ConventionsInput,
|
|
@@ -132,17 +133,17 @@ function toAddress(className: string): string {
|
|
|
132
133
|
* The container key a constructor parameter asks for — derived from its TYPE, not from
|
|
133
134
|
* how the type was spelled.
|
|
134
135
|
*
|
|
135
|
-
* `deps` used to be `p.type.name`, so the key WAS the alias's name: `type
|
|
136
|
-
*
|
|
137
|
-
* registration key (`
|
|
138
|
-
* typechecked and died at boot on `'ListRepo' is not registered`. And `
|
|
139
|
-
* written out in full asked for `'
|
|
136
|
+
* `deps` used to be `p.type.name`, so the key WAS the alias's name: `type ListStorage =
|
|
137
|
+
* Storage<List>` resolved only because someone had spelled it exactly like the
|
|
138
|
+
* registration key (`ListStorage`), while `type ListRepo = Storage<List>` — the same type —
|
|
139
|
+
* typechecked and died at boot on `'ListRepo' is not registered`. And `Storage<List>`
|
|
140
|
+
* written out in full asked for `'Storage'`, which nothing registers.
|
|
140
141
|
*
|
|
141
|
-
* `
|
|
142
|
-
* parsed (`
|
|
142
|
+
* `Storage<X>` names X's storage, so that is the key. The generic argument was already
|
|
143
|
+
* parsed (`TypeRef.generics`) and thrown away. Anything else keeps its own name: a
|
|
143
144
|
* plain service IS designated by its class name.
|
|
144
145
|
*/
|
|
145
|
-
function depKeyOf(type:
|
|
146
|
+
function depKeyOf(type: TypeRef): string {
|
|
146
147
|
// `Facade<PostHandler>` — the second port, read exactly like the first. The type names
|
|
147
148
|
// what arrives: not the handler (its methods take positional arguments and it is never
|
|
148
149
|
// injected), but the door built in front of it. Same key whether that door is the local
|
|
@@ -168,10 +169,10 @@ function depKeyOf(type: ParsedType): string {
|
|
|
168
169
|
const held = type.name === 'RepositoryOf' ? type.generics?.[0]?.name : undefined;
|
|
169
170
|
if (held) return repositoryKeyOf(held);
|
|
170
171
|
|
|
171
|
-
const target = type.name === '
|
|
172
|
+
const target = type.name === 'Storage' ? type.generics?.[0]?.name : undefined;
|
|
172
173
|
if (!target) return type.name;
|
|
173
174
|
|
|
174
|
-
return
|
|
175
|
+
return storageKeyOf(target);
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/**
|
|
@@ -179,7 +180,7 @@ function depKeyOf(type: ParsedType): string {
|
|
|
179
180
|
*
|
|
180
181
|
* The tuple was chosen over the variadic form the parser reads more cleanly, because the
|
|
181
182
|
* variadic one costs arities-with-defaults and a `never` filter on the TypeScript side
|
|
182
|
-
* while the tuple maps to `[
|
|
183
|
+
* while the tuple maps to `[Storage<Account>, Storage<Ledger>]` in one line. The
|
|
183
184
|
* parser does not get to decide alone; this split is what that choice costs.
|
|
184
185
|
*/
|
|
185
186
|
function tupleMembers(raw: string): string[] {
|
|
@@ -216,9 +217,9 @@ async function toProvider(filePath: string): Promise<ProviderEntry> {
|
|
|
216
217
|
const owned = ownedBy(ctor);
|
|
217
218
|
const target = targetOf(ctor);
|
|
218
219
|
if (owned.length > 1 && deps.length === 0) {
|
|
219
|
-
deps.push(...owned.map((entity) =>
|
|
220
|
+
deps.push(...owned.map((entity) => storageKeyOf(lowerFirst((entity as { name: string }).name))));
|
|
220
221
|
} else if (target && deps.length === 0) {
|
|
221
|
-
deps.push(
|
|
222
|
+
deps.push(storageKeyOf(lowerFirst((target as { name: string }).name)));
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// No `name` beside `ctor`: a provider registers under `ctor.name`, which is what
|
|
@@ -243,10 +244,10 @@ async function toEntityEntry(filePath: string): Promise<EntityEntry | null> {
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
/**
|
|
246
|
-
* Resolve a
|
|
247
|
+
* Resolve a TypeRef to a runtime schema if available in module exports.
|
|
247
248
|
* Handles arrays, generics (uses base name), and simple references.
|
|
248
249
|
*/
|
|
249
|
-
function resolveSchema(type:
|
|
250
|
+
function resolveSchema(type: TypeRef, moduleExports: Record<string, unknown>): SchemaView | undefined {
|
|
250
251
|
// An array's element type IS `type.name` — the arity rides beside it, so nothing has
|
|
251
252
|
// to be unwrapped here.
|
|
252
253
|
// For generics like Pagination<Post>, also check inner types
|
|
@@ -751,7 +752,29 @@ export async function scanProject(
|
|
|
751
752
|
|
|
752
753
|
// The app's own domain first, then the ones it took in.
|
|
753
754
|
const all = rootFrond ? [rootFrond, ...under] : under;
|
|
754
|
-
const fronds = Fronds.
|
|
755
|
+
const fronds = Fronds.hosting(filter ? all.filter((f) => filter.includes(f.name)) : all);
|
|
755
756
|
|
|
756
757
|
return { fronds, diagnostics };
|
|
757
758
|
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* What changes when a frond's domain changes — the paths a dev loop watches.
|
|
762
|
+
*
|
|
763
|
+
* It lives beside the scan rather than beside the conventions it reads, because it turns
|
|
764
|
+
* names into DISK paths: `join` is `node:path`, and `conventions.ts` is reached from
|
|
765
|
+
* `index` through `frond()`, which an edge bundle imports. Measured — esbuild refused the
|
|
766
|
+
* Worker with `Could not resolve "node:path"`.
|
|
767
|
+
*
|
|
768
|
+
* The root frond IS the scan root, so watching its path would match every write in the
|
|
769
|
+
* project: `.nuxt/`, `node_modules/`, the build output. Its convention directories are
|
|
770
|
+
* the frond, and they are what a scan re-reads.
|
|
771
|
+
*/
|
|
772
|
+
export function watchPathsOf(
|
|
773
|
+
frond: { source: { path: string } },
|
|
774
|
+
scanRoot: string,
|
|
775
|
+
conventions: Conventions,
|
|
776
|
+
): string[] {
|
|
777
|
+
return frond.source.path === scanRoot
|
|
778
|
+
? frondDirsOf(conventions).map((dir) => join(scanRoot, dir))
|
|
779
|
+
: [frond.source.path];
|
|
780
|
+
}
|
package/src/scan/statement.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* — same rows, same computed fields, and no `typescript` loaded at boot.
|
|
19
19
|
*/
|
|
20
20
|
import { relative } from 'node:path';
|
|
21
|
-
import type { FrondDescriptor
|
|
21
|
+
import type { FrondDescriptor } from '../descriptor/frond.js';
|
|
22
|
+
import type { ScanResult } from './result.js';
|
|
22
23
|
|
|
23
24
|
type Live = { name?: string };
|
|
24
25
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { upperFirst, type SchemaView } from '@fougere/schema';
|
|
1
|
+
import { upperFirst, lowerFirst, type SchemaView } from '@fougere/schema';
|
|
2
2
|
|
|
3
3
|
/** Options for list queries — pagination, sorting, counting. */
|
|
4
4
|
export interface ListOptions {
|
|
@@ -68,8 +68,8 @@ export interface SelectOption {
|
|
|
68
68
|
select?: SchemaView;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Per-entity
|
|
72
|
-
export interface
|
|
71
|
+
/** Per-entity storage — scoped CRUD operations on a single entity type. */
|
|
72
|
+
export interface Storage<T = Record<string, unknown>> {
|
|
73
73
|
list(options?: ListOptions & SelectOption): Promise<ListResult<T>>;
|
|
74
74
|
findById(id: string, options?: SelectOption): Promise<T | undefined>;
|
|
75
75
|
/**
|
|
@@ -78,7 +78,7 @@ export interface EntityOrm<T = Record<string, unknown>> {
|
|
|
78
78
|
*
|
|
79
79
|
* Both existed on the SQL implementation from the start and neither was declared here.
|
|
80
80
|
* A port that hides what it offers is a port nobody can use: `auth-better` cast its way
|
|
81
|
-
* in (`
|
|
81
|
+
* in (`storage as StorageWithFindBy`), a presenter that needed the lines of an order read the
|
|
82
82
|
* whole table instead, and the GraphQL relation resolver passed criteria to `list()`
|
|
83
83
|
* — which drops what it does not know.
|
|
84
84
|
*/
|
|
@@ -137,15 +137,15 @@ export interface EntityOrm<T = Record<string, unknown>> {
|
|
|
137
137
|
upsertAll(inputs: readonly Partial<T>[], options?: SelectOption): Promise<number>;
|
|
138
138
|
update(id: string, input: Partial<T>, options?: SelectOption): Promise<T>;
|
|
139
139
|
delete(id: string): Promise<boolean>;
|
|
140
|
-
/** Returns a scoped
|
|
141
|
-
output(schema: SchemaView):
|
|
140
|
+
/** Returns a scoped storage that restricts all read results to the fields of the given schema. */
|
|
141
|
+
output(schema: SchemaView): Storage<T>;
|
|
142
142
|
/**
|
|
143
|
-
* What this
|
|
143
|
+
* What this storage wraps — the Kysely instance for the SQL one, something else elsewhere.
|
|
144
144
|
*
|
|
145
|
-
* Every judge sits on the
|
|
145
|
+
* Every judge sits on the storage's own methods, so a statement issued here meets none of
|
|
146
146
|
* them: a value the entity refuses lands in the table without a word. It is the port's
|
|
147
147
|
* own escape hatch rather than a handle on the side, so it keeps the scope the container
|
|
148
|
-
* gave you — `
|
|
148
|
+
* gave you — `productStorage.client` reaches the products, not the whole database.
|
|
149
149
|
*
|
|
150
150
|
* `unknown` on purpose: the client belongs to the implementation, and narrowing it is
|
|
151
151
|
* the caller saying out loud which one they are standing on.
|
|
@@ -154,13 +154,106 @@ export interface EntityOrm<T = Record<string, unknown>> {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Factory that creates
|
|
157
|
+
* Factory that creates a Storage for a given entity.
|
|
158
158
|
* Called by bootstrap for every scanned entity.
|
|
159
159
|
*/
|
|
160
|
-
export type
|
|
160
|
+
export type StorageFactory = (entity: SchemaView, name: string) => Storage;
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
*
|
|
163
|
+
* A place rows live, whatever realizes it.
|
|
164
|
+
*
|
|
165
|
+
* `StorageFactory` plus its lifecycle, which is why it lives beside it. Three of the four
|
|
166
|
+
* gestures are optional and ABSENCE IS THE ANSWER: no `transacted` and a frame compensates
|
|
167
|
+
* instead of transacting (`boot/together.ts`), no `migrate` and there is no shape to bring
|
|
168
|
+
* up to date. What a source is MADE OF is not here — `adapter/sql` states `dialect`, `db`
|
|
169
|
+
* and `sink` on its own `SqlSource`, reached by narrowing, the rule {@link Storage.client}
|
|
170
|
+
* already obeys one level down.
|
|
171
|
+
*/
|
|
172
|
+
export interface Source {
|
|
173
|
+
storageFactory: StorageFactory;
|
|
174
|
+
/** Bring the shape of what lives here up to date. */
|
|
175
|
+
migrate?(view: SourceView): Promise<void>;
|
|
176
|
+
/** Run `fn` as ONE unit of work, with a factory bound to it. */
|
|
177
|
+
transacted?<R>(fn: (factory: StorageFactory) => Promise<R>): Promise<R>;
|
|
178
|
+
close?(): Promise<void>;
|
|
179
|
+
/** What distinguishes it when a query is reported. */
|
|
180
|
+
name?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The app as ONE source sees it: what lives there, and the NAMES of what does not.
|
|
185
|
+
*
|
|
186
|
+
* The second half is what lets a DDL stop lying. A batch holding every entity cannot tell
|
|
187
|
+
* a cross-source target from a typo, so a `ref()` falls back to a derived table name and
|
|
188
|
+
* the constraint is emitted against a table that may not exist. `elsewhere` says which
|
|
189
|
+
* misses are legitimate — and a target in neither list is a mistake, out loud.
|
|
190
|
+
*/
|
|
191
|
+
export interface SourceView {
|
|
192
|
+
fronds: readonly { readonly name: string; readonly entities: readonly { readonly name: string }[] }[];
|
|
193
|
+
/** The auth provider's own entities, when they ride with this source. */
|
|
194
|
+
auth?: unknown;
|
|
195
|
+
elsewhere: readonly string[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* What a config file can carry about a source — values, never a live driver.
|
|
200
|
+
*
|
|
201
|
+
* `source` names the ADAPTER, and everything else belongs to the adapter it named:
|
|
202
|
+
* `adapter/sql` reads `dialect`, a file one reads `path`. That is the shape
|
|
203
|
+
* `EntityAdapters` already has — addressed by adapter, and what sits below is the
|
|
204
|
+
* adapter's own. Which is why the level `schema` owns is not repeated here.
|
|
205
|
+
*/
|
|
206
|
+
export interface SourceConfig {
|
|
207
|
+
/** The adapter that realizes it. Absent means the conventional one. */
|
|
208
|
+
source?: string;
|
|
209
|
+
/** The entities whose rows live here. Absent on the default source: it holds the rest. */
|
|
210
|
+
entities?: string[];
|
|
211
|
+
[key: string]: unknown;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Which adapter answers a source name — one per process, no subject to hold.
|
|
216
|
+
*
|
|
217
|
+
* The shape `Generators` already has, and the refusal `resolveStorage` already made: only
|
|
218
|
+
* `sqlite` could be resolved from its name, because it was the one driver `defaults`
|
|
219
|
+
* depended on. That refusal had no owner — it lived in the package that happened to import
|
|
220
|
+
* the driver — so a second adapter had nowhere to say it exists. Here it does, and an
|
|
221
|
+
* unknown name is refused naming what this process answers.
|
|
222
|
+
*
|
|
223
|
+
* Registered at IMPORT by each adapter, so nothing central lists them.
|
|
224
|
+
*/
|
|
225
|
+
export class Sources {
|
|
226
|
+
private static readonly registry = new Map<string, (conf: SourceConfig) => Source>();
|
|
227
|
+
|
|
228
|
+
static register(name: string, build: (conf: SourceConfig) => Source): void {
|
|
229
|
+
this.registry.set(name, build);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The source this name stands for, refused by name when nothing does. */
|
|
233
|
+
static resolve(name: string, conf: SourceConfig): Source {
|
|
234
|
+
const build = this.registry.get(name);
|
|
235
|
+
if (build) return build(conf);
|
|
236
|
+
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Unknown source '${name}' — import the adapter that answers it, or register one with `
|
|
239
|
+
+ `Sources.register('${name}', build). This process answers `
|
|
240
|
+
+ `${[...this.registry.keys()].join(', ') || 'nothing yet'}.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Whether a name is answered, for a caller that must not throw to find out. */
|
|
245
|
+
static answers(name: string): boolean {
|
|
246
|
+
return this.registry.has(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What this process answers — what a refusal elsewhere prints. */
|
|
250
|
+
static answered(): string[] {
|
|
251
|
+
return [...this.registry.keys()];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Container key of an entity's storage — 'reading' → 'ReadingStorage'.
|
|
164
257
|
*
|
|
165
258
|
* The twin of {@link repositoryKeyOf} and of `facadeKeyOf`: the key of a thing
|
|
166
259
|
* lives with the thing. This one was spelled by hand in four places in
|
|
@@ -168,14 +261,30 @@ export type OrmFactory = (entity: SchemaView, name: string) => EntityOrm;
|
|
|
168
261
|
* constructor asks for. Two readers of one convention, neither of them naming it,
|
|
169
262
|
* so a rename would have moved one and left the other resolving to nothing.
|
|
170
263
|
*/
|
|
171
|
-
export function
|
|
172
|
-
return `${upperFirst(entity)}
|
|
264
|
+
export function storageKeyOf(entity: string): string {
|
|
265
|
+
return `${upperFirst(entity)}${HELD}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The entity behind a storage key, or `undefined` when the key is not one.
|
|
270
|
+
*
|
|
271
|
+
* The dual of the line above, for the reason `membersOfTogetherKey` is. `known` is what the
|
|
272
|
+
* suffix alone cannot answer: `FileStorage` is an ordinary class name.
|
|
273
|
+
*/
|
|
274
|
+
export function entityOfStorageKey(key: string, known: (entity: string) => boolean): string | undefined {
|
|
275
|
+
if (key.length <= HELD.length || !key.endsWith(HELD)) return undefined;
|
|
276
|
+
const entity = lowerFirst(key.slice(0, -HELD.length));
|
|
277
|
+
|
|
278
|
+
return storageKeyOf(entity) === key && known(entity) ? entity : undefined;
|
|
173
279
|
}
|
|
174
280
|
|
|
281
|
+
/** What a holder keeps, said in the key it is registered under. */
|
|
282
|
+
const HELD = 'Storage';
|
|
283
|
+
|
|
175
284
|
/**
|
|
176
285
|
* `Together<[Account, Ledger]>` — writes that stand or fall as one.
|
|
177
286
|
*
|
|
178
|
-
* `
|
|
287
|
+
* `Storage` is the port whose every gesture is ONE statement, and one statement is
|
|
179
288
|
* atomic in every engine. This is the port whose unit is a BLOCK: what the callback did
|
|
180
289
|
* happens entirely, or not at all. Nothing else separates them — the arity of the unit
|
|
181
290
|
* of work is the whole distinction, which is why this lives here and not in a file of
|
|
@@ -191,7 +300,7 @@ export function ormKeyOf(entity: string): string {
|
|
|
191
300
|
* ```
|
|
192
301
|
*
|
|
193
302
|
* **The second list is providers**, rebuilt inside the frame so that what THEY write is
|
|
194
|
-
* covered too — a `Mirror` writes its pages through `
|
|
303
|
+
* covered too — a `Mirror` writes its pages through `Storage<T>`, so naming it puts
|
|
195
304
|
* them under the same unwind, with no locator and no second injection path:
|
|
196
305
|
*
|
|
197
306
|
* ```ts
|
|
@@ -214,7 +323,7 @@ export function ormKeyOf(entity: string): string {
|
|
|
214
323
|
* than letting the author assume the stronger one.
|
|
215
324
|
*/
|
|
216
325
|
export interface Together<E extends readonly unknown[], P extends readonly unknown[] = []> {
|
|
217
|
-
run<R>(fn: (entities: { [K in keyof E]:
|
|
326
|
+
run<R>(fn: (entities: { [K in keyof E]: Storage<E[K]> }, providers: P) => Promise<R>): Promise<R>;
|
|
218
327
|
}
|
|
219
328
|
|
|
220
329
|
/**
|
package/src/verify.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { FrondDescriptor } from './
|
|
1
|
+
import type { FrondDescriptor } from './descriptor/frond.js';
|
|
2
2
|
import { lowerFirst } from '@fougere/schema';
|
|
3
3
|
import { repositoryKeyOf } from './prefab/repository.js';
|
|
4
|
-
import {
|
|
4
|
+
import { storageKeyOf } from './storage.js';
|
|
5
5
|
import { presenterKeyOf } from './prefab/presenter.js';
|
|
6
6
|
import { collectorKeyOf } from './prefab/collector.js';
|
|
7
7
|
|
|
@@ -63,7 +63,7 @@ function registrationsOf(frond: FrondDescriptor): Map<string, Registration> {
|
|
|
63
63
|
for (const p of frond.presenters) put(presenterKeyOf(p.entityName), 'presenter');
|
|
64
64
|
for (const c of frond.collectors) put(collectorKeyOf(c.typeName), 'collector');
|
|
65
65
|
for (const e of frond.entities) {
|
|
66
|
-
put(
|
|
66
|
+
put(storageKeyOf(e.name), 'storage');
|
|
67
67
|
put(repositoryKeyOf(e.name), 'repository');
|
|
68
68
|
}
|
|
69
69
|
return out;
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* be repeated here too, and it had already lost `Fact` — the one whose position is
|
|
7
7
|
* load-bearing, since the fall-through would otherwise hand it the caller's body.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
10
|
-
import type { InvocationContext } from '../contract/Invocation.js';
|
|
9
|
+
import type { Param } from './signature.js';
|
|
11
10
|
import { lowerFirst } from '@fougere/schema';
|
|
12
11
|
|
|
13
12
|
// ── Types ─────────────────────────────────────
|
|
@@ -56,7 +55,7 @@ function coercionFor(typeName: string): 'number' | 'boolean' | undefined {
|
|
|
56
55
|
* @param collectorTypeNames - Registration keys of the types a Collector answers for
|
|
57
56
|
*/
|
|
58
57
|
export function computeBindingPlan(
|
|
59
|
-
params:
|
|
58
|
+
params: Param[],
|
|
60
59
|
collectorTypeNames: Set<string>,
|
|
61
60
|
): BindingPlan {
|
|
62
61
|
return params.map((param) => {
|
|
@@ -117,66 +116,3 @@ export function computeBindingPlan(
|
|
|
117
116
|
};
|
|
118
117
|
});
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
// ── Resolve ───────────────────────────────────
|
|
122
|
-
|
|
123
|
-
export interface CollectorResolver {
|
|
124
|
-
collect(ctx: InvocationContext): Promise<unknown>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Resolve handler arguments from a BindingPlan + InvocationContext.
|
|
129
|
-
*/
|
|
130
|
-
export async function resolveArgs(
|
|
131
|
-
plan: BindingPlan,
|
|
132
|
-
ctx: InvocationContext,
|
|
133
|
-
resolveCollector?: (typeName: string) => CollectorResolver | undefined,
|
|
134
|
-
): Promise<unknown[]> {
|
|
135
|
-
const args: unknown[] = [];
|
|
136
|
-
|
|
137
|
-
for (const binding of plan) {
|
|
138
|
-
switch (binding.source.kind) {
|
|
139
|
-
case 'collector': {
|
|
140
|
-
const collector = resolveCollector?.(binding.source.typeName);
|
|
141
|
-
args.push(collector ? await collector.collect(ctx) : undefined);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
case 'context': {
|
|
145
|
-
args.push(ctx);
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case 'param': {
|
|
149
|
-
// `null` is a value, not a miss. Nullish coalescing used to make an explicit
|
|
150
|
-
// nullable path/GraphQL argument fall through to query (or become undefined),
|
|
151
|
-
// collapsing `T | null` into `T | undefined`. Only undefined means absent.
|
|
152
|
-
const fromParams = ctx.params[binding.source.name];
|
|
153
|
-
let val: unknown = fromParams === undefined
|
|
154
|
-
? ctx.query[binding.source.name]
|
|
155
|
-
: fromParams;
|
|
156
|
-
if (val != null && binding.source.coerce === 'number') val = Number(val);
|
|
157
|
-
if (val != null && binding.source.coerce === 'boolean') val = val === 'true' || val === '1' || val === true;
|
|
158
|
-
args.push(val);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case 'fact': {
|
|
162
|
-
// A fact IS the payload — the whole of what happened, never a piece of it.
|
|
163
|
-
// Identical to `body` today, and deliberately not sharing its branch: the two
|
|
164
|
-
// agree by coincidence, not by rule, and the day `body` learns to look up a
|
|
165
|
-
// value by parameter name a subscriber would receive ONE FIELD of the fact it
|
|
166
|
-
// subscribed to. Splitting it costs nothing and removes that trap.
|
|
167
|
-
args.push(ctx.body);
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
case 'body': {
|
|
171
|
-
args.push(ctx.body);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
case 'query': {
|
|
175
|
-
args.push(ctx.query);
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return args;
|
|
182
|
-
}
|