@fougere/core 0.1.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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/binding.d.ts +63 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +107 -0
- package/dist/binding.js.map +1 -0
- package/dist/boot.d.ts +38 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +53 -0
- package/dist/boot.js.map +1 -0
- package/dist/bootstrap.d.ts +4 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +825 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/builtins/config.d.ts +4 -0
- package/dist/builtins/config.d.ts.map +1 -0
- package/dist/builtins/config.js +6 -0
- package/dist/builtins/config.js.map +1 -0
- package/dist/builtins/logger.d.ts +28 -0
- package/dist/builtins/logger.d.ts.map +1 -0
- package/dist/builtins/logger.js +76 -0
- package/dist/builtins/logger.js.map +1 -0
- package/dist/call.d.ts +172 -0
- package/dist/call.d.ts.map +1 -0
- package/dist/call.js +209 -0
- package/dist/call.js.map +1 -0
- package/dist/collector.d.ts +28 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/collector.js +32 -0
- package/dist/collector.js.map +1 -0
- package/dist/config-loader.d.ts +59 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +97 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/contract.d.ts +43 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +29 -0
- package/dist/contract.js.map +1 -0
- package/dist/crud.d.ts +61 -0
- package/dist/crud.d.ts.map +1 -0
- package/dist/crud.js +85 -0
- package/dist/crud.js.map +1 -0
- package/dist/define.d.ts +8 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +9 -0
- package/dist/define.js.map +1 -0
- package/dist/egress.d.ts +78 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +185 -0
- package/dist/egress.js.map +1 -0
- package/dist/emit.d.ts +51 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +57 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/frond-config.d.ts +80 -0
- package/dist/frond-config.d.ts.map +1 -0
- package/dist/frond-config.js +32 -0
- package/dist/frond-config.js.map +1 -0
- package/dist/graph.d.ts +33 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +142 -0
- package/dist/graph.js.map +1 -0
- package/dist/handler-parser.d.ts +82 -0
- package/dist/handler-parser.d.ts.map +1 -0
- package/dist/handler-parser.js +505 -0
- package/dist/handler-parser.js.map +1 -0
- package/dist/imports.d.ts +22 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +80 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/invocation.d.ts +20 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +15 -0
- package/dist/invocation.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +15 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +29 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/logger.d.ts +14 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +25 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware.d.ts +75 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +89 -0
- package/dist/middleware.js.map +1 -0
- package/dist/operation.d.ts +87 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +95 -0
- package/dist/operation.js.map +1 -0
- package/dist/orm.d.ts +96 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +33 -0
- package/dist/orm.js.map +1 -0
- package/dist/presenter.d.ts +63 -0
- package/dist/presenter.d.ts.map +1 -0
- package/dist/presenter.js +72 -0
- package/dist/presenter.js.map +1 -0
- package/dist/remote.d.ts +48 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +135 -0
- package/dist/remote.js.map +1 -0
- package/dist/repository.d.ts +55 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +26 -0
- package/dist/repository.js.map +1 -0
- package/dist/scan-cache.d.ts +10 -0
- package/dist/scan-cache.d.ts.map +1 -0
- package/dist/scan-cache.js +92 -0
- package/dist/scan-cache.js.map +1 -0
- package/dist/scanner.d.ts +34 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +595 -0
- package/dist/scanner.js.map +1 -0
- package/dist/seed.d.ts +28 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +110 -0
- package/dist/seed.js.map +1 -0
- package/dist/types.d.ts +387 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +57 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +147 -0
- package/dist/verify.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector(Entity) — resolves handler input parameters from invocation context.
|
|
3
|
+
*
|
|
4
|
+
* The dual of Presenter: Presenter enriches output, Collector resolves input.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export default class UserCollector extends Collector(User) {
|
|
9
|
+
* constructor(private userOrm: UserOrm) { super(); }
|
|
10
|
+
*
|
|
11
|
+
* async collect(ctx: InvocationContext) {
|
|
12
|
+
* return this.userOrm.findById(ctx.state.userId as string);
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
const COLLECTOR_TARGET = Symbol.for('fougere:collector_target');
|
|
18
|
+
export function Collector(entity) {
|
|
19
|
+
class CollectorBase {
|
|
20
|
+
static [COLLECTOR_TARGET] = entity;
|
|
21
|
+
}
|
|
22
|
+
return CollectorBase;
|
|
23
|
+
}
|
|
24
|
+
/** Get the entity class a collector targets. */
|
|
25
|
+
export function getCollectorTarget(ctor) {
|
|
26
|
+
return ctor[COLLECTOR_TARGET];
|
|
27
|
+
}
|
|
28
|
+
/** Container key of an entity's collector — 'user' → 'UserCollector'. */
|
|
29
|
+
export function collectorKeyOf(entity) {
|
|
30
|
+
return `${entity[0].toUpperCase()}${entity.slice(1)}Collector`;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.js","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;AAEhE,MAAM,UAAU,SAAS,CAAwB,MAAS;IACxD,MAAM,aAAa;QACjB,MAAM,CAAC,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAAC;;IAErC,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,kBAAkB,CAAC,IAAc;IAC/C,OAAQ,IAAY,CAAC,gBAAgB,CAAC,CAAC;AACzC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC;AACjE,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AuthConfig } from './types.js';
|
|
2
|
+
export interface FougereConfig {
|
|
3
|
+
/** Database configuration. */
|
|
4
|
+
db?: 'sqlite' | {
|
|
5
|
+
dialect: 'sqlite';
|
|
6
|
+
path?: string;
|
|
7
|
+
} | false;
|
|
8
|
+
/** Directory containing fronds. Defaults to 'fronds'. */
|
|
9
|
+
frondsDir?: string;
|
|
10
|
+
/** Remote fronds — frondName → base URL. */
|
|
11
|
+
remotes?: Record<string, string>;
|
|
12
|
+
/** Auth declaration — picks a provider package and forwards options to it. */
|
|
13
|
+
auth?: AuthConfig;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedConfig {
|
|
16
|
+
/** Global config merged with all overrides. */
|
|
17
|
+
global: FougereConfig;
|
|
18
|
+
/** Per-frond config overrides. */
|
|
19
|
+
fronds: Record<string, FougereConfig>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load the root fougere.config.{ts,js,mjs} from the given directory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadConfig(root: string): Promise<FougereConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* Load per-frond config files from each frond's directory.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadFrondConfigs(root: string, frondsDir?: string): Promise<Record<string, FougereConfig>>;
|
|
29
|
+
/**
|
|
30
|
+
* Override a config with another, the invariant of every cascade level:
|
|
31
|
+
* scalar keys replace, but `remotes` (the topology) MERGES — an override adds
|
|
32
|
+
* or redirects a frond without erasing the others. Used for workspace→app and
|
|
33
|
+
* for global→CLI alike.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergeGlobal(base: FougereConfig, override: Partial<FougereConfig>): FougereConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Merge configs with cascade: global → frond → CLI overrides.
|
|
38
|
+
*
|
|
39
|
+
* Returns a ResolvedConfig with the merged global and per-frond overrides.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeConfig(global: FougereConfig, frondConfigs: Record<string, FougereConfig>, cliOverrides?: Partial<FougereConfig>): ResolvedConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Get the effective config for a specific frond.
|
|
44
|
+
* Cascade: global → frond override.
|
|
45
|
+
*/
|
|
46
|
+
export declare function configForFrond(resolved: ResolvedConfig, frondName: string): FougereConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Load config along the workspace→app frontier. The workspace-root config is
|
|
49
|
+
* the base (canonical topology: `remotes`, shared `db`); the app-root config
|
|
50
|
+
* overrides. Same `root` boundary the fronds already cascade along. When both
|
|
51
|
+
* roots resolve to the same dir (single app, no workspace), this is the plain
|
|
52
|
+
* root config — idempotent, no behavior change.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadCascadedConfig(workspaceRoot: string, appRoot: string): Promise<FougereConfig>;
|
|
55
|
+
/**
|
|
56
|
+
* Load everything: root config + frond configs + merge with CLI overrides.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveConfig(root: string, cliOverrides?: Partial<FougereConfig>): Promise<ResolvedConfig>;
|
|
59
|
+
//# sourceMappingURL=config-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK7C,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,EAAE,CAAC,EAAE,QAAQ,GAAG;QAAE,OAAO,EAAE,QAAQ,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAC7D,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,MAAM,EAAE,aAAa,CAAC;IACtB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACvC;AAkBD;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAErE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAgBjH;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAMhG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC3C,YAAY,GAAE,OAAO,CAAC,aAAa,CAAM,GACxC,cAAc,CAEhB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CAIzF;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAIvG;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,OAAO,CAAC,aAAa,CAAM,GACxC,OAAO,CAAC,cAAc,CAAC,CAKzB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import { getModuleLoader } from './scanner.js';
|
|
5
|
+
// ── Loading ──────────────────────────────────────
|
|
6
|
+
const CONFIG_FILES = ['fougere.config.ts', 'fougere.config.js', 'fougere.config.mjs'];
|
|
7
|
+
async function loadConfigFrom(dir) {
|
|
8
|
+
const loader = getModuleLoader();
|
|
9
|
+
for (const file of CONFIG_FILES) {
|
|
10
|
+
const path = resolve(dir, file);
|
|
11
|
+
if (existsSync(path)) {
|
|
12
|
+
const mod = await loader(path);
|
|
13
|
+
return (mod.default ?? mod);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load the root fougere.config.{ts,js,mjs} from the given directory.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadConfig(root) {
|
|
22
|
+
return loadConfigFrom(root);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load per-frond config files from each frond's directory.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadFrondConfigs(root, frondsDir = 'fronds') {
|
|
28
|
+
const dir = join(root, frondsDir);
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
return {};
|
|
31
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
32
|
+
const configs = {};
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
const frondConfig = await loadConfigFrom(join(dir, entry.name));
|
|
37
|
+
if (Object.keys(frondConfig).length > 0) {
|
|
38
|
+
configs[entry.name] = frondConfig;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return configs;
|
|
42
|
+
}
|
|
43
|
+
// ── Merging ──────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Override a config with another, the invariant of every cascade level:
|
|
46
|
+
* scalar keys replace, but `remotes` (the topology) MERGES — an override adds
|
|
47
|
+
* or redirects a frond without erasing the others. Used for workspace→app and
|
|
48
|
+
* for global→CLI alike.
|
|
49
|
+
*/
|
|
50
|
+
export function mergeGlobal(base, override) {
|
|
51
|
+
const merged = { ...base, ...override };
|
|
52
|
+
if (base.remotes || override.remotes) {
|
|
53
|
+
merged.remotes = { ...base.remotes, ...override.remotes };
|
|
54
|
+
}
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Merge configs with cascade: global → frond → CLI overrides.
|
|
59
|
+
*
|
|
60
|
+
* Returns a ResolvedConfig with the merged global and per-frond overrides.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeConfig(global, frondConfigs, cliOverrides = {}) {
|
|
63
|
+
return { global: mergeGlobal(global, cliOverrides), fronds: frondConfigs };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the effective config for a specific frond.
|
|
67
|
+
* Cascade: global → frond override.
|
|
68
|
+
*/
|
|
69
|
+
export function configForFrond(resolved, frondName) {
|
|
70
|
+
const frondOverride = resolved.fronds[frondName];
|
|
71
|
+
if (!frondOverride)
|
|
72
|
+
return resolved.global;
|
|
73
|
+
return { ...resolved.global, ...frondOverride };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load config along the workspace→app frontier. The workspace-root config is
|
|
77
|
+
* the base (canonical topology: `remotes`, shared `db`); the app-root config
|
|
78
|
+
* overrides. Same `root` boundary the fronds already cascade along. When both
|
|
79
|
+
* roots resolve to the same dir (single app, no workspace), this is the plain
|
|
80
|
+
* root config — idempotent, no behavior change.
|
|
81
|
+
*/
|
|
82
|
+
export async function loadCascadedConfig(workspaceRoot, appRoot) {
|
|
83
|
+
const base = await loadConfig(workspaceRoot);
|
|
84
|
+
if (resolve(workspaceRoot) === resolve(appRoot))
|
|
85
|
+
return base;
|
|
86
|
+
return mergeGlobal(base, await loadConfig(appRoot));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Load everything: root config + frond configs + merge with CLI overrides.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveConfig(root, cliOverrides = {}) {
|
|
92
|
+
const global = await loadConfig(root);
|
|
93
|
+
const frondsDir = cliOverrides.frondsDir ?? global.frondsDir ?? 'fronds';
|
|
94
|
+
const frondConfigs = await loadFrondConfigs(root, frondsDir);
|
|
95
|
+
return mergeConfig(global, frondConfigs, cliOverrides);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=config-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract surface — what a sender needs to speak to a receiver, and
|
|
3
|
+
* nothing that assumes a runtime: the error vocabulary, the invocation
|
|
4
|
+
* shape, the call value, the naming convention.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe by design: no node builtin may enter this module's import
|
|
7
|
+
* graph. Published as the `@fougere/core/contract` subpath so client
|
|
8
|
+
* bundles never touch the full index (scanner, config loader).
|
|
9
|
+
*/
|
|
10
|
+
import { type InvocationContext } from './invocation.js';
|
|
11
|
+
import type { FrondCall } from './call.js';
|
|
12
|
+
export { FougereError, ErrorCode } from './middleware.js';
|
|
13
|
+
export type { FougereErrorOptions } from './middleware.js';
|
|
14
|
+
export { EMPTY_INVOCATION } from './invocation.js';
|
|
15
|
+
export type { InvocationContext } from './invocation.js';
|
|
16
|
+
export type { FrondCall, Transport } from './call.js';
|
|
17
|
+
/**
|
|
18
|
+
* What `rpc.discover` answers. It belongs here and not to the runtime: the
|
|
19
|
+
* reserved op travels on the same wire as every other call, so a consumer that
|
|
20
|
+
* only sends — a browser bundle, a frond written elsewhere — needs its shape and
|
|
21
|
+
* nothing else. Type-only, so `call.js` never enters the runtime graph.
|
|
22
|
+
*
|
|
23
|
+
* Stated once, on purpose: two private copies of this interface have already gone
|
|
24
|
+
* stale (the CLI's, then the Rust demo's) the day an op stopped being a bare name.
|
|
25
|
+
*/
|
|
26
|
+
export type { IdentityCard, CardOp } from './call.js';
|
|
27
|
+
/** Registration key of a class — 'Post' → 'post', 'PostHandler' → 'postHandler'. */
|
|
28
|
+
export declare function toRegistrationName(name: string): string;
|
|
29
|
+
/** A call, fully fabricated: the designation and its completed invocation. */
|
|
30
|
+
export interface CallValue {
|
|
31
|
+
call: FrondCall;
|
|
32
|
+
invocation: InvocationContext;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fabricate the call value — one designation, two spellings:
|
|
36
|
+
* `callValueOf(Post, 'list', { query })` (class + verb) or
|
|
37
|
+
* `callValueOf({ entity, op }, input)` (raw, for dynamic bridges).
|
|
38
|
+
* Missing invocation fields complete to the empty invocation.
|
|
39
|
+
*/
|
|
40
|
+
export declare function callValueOf(target: {
|
|
41
|
+
name: string;
|
|
42
|
+
} | FrondCall, opOrInput?: string | Partial<InvocationContext>, input?: Partial<InvocationContext>): CallValue;
|
|
43
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtD;;;;;;;;GAQG;AACH,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACpC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,EAC/C,KAAK,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACjC,SAAS,CAMX"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract surface — what a sender needs to speak to a receiver, and
|
|
3
|
+
* nothing that assumes a runtime: the error vocabulary, the invocation
|
|
4
|
+
* shape, the call value, the naming convention.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe by design: no node builtin may enter this module's import
|
|
7
|
+
* graph. Published as the `@fougere/core/contract` subpath so client
|
|
8
|
+
* bundles never touch the full index (scanner, config loader).
|
|
9
|
+
*/
|
|
10
|
+
import { EMPTY_INVOCATION } from './invocation.js';
|
|
11
|
+
export { FougereError, ErrorCode } from './middleware.js';
|
|
12
|
+
export { EMPTY_INVOCATION } from './invocation.js';
|
|
13
|
+
/** Registration key of a class — 'Post' → 'post', 'PostHandler' → 'postHandler'. */
|
|
14
|
+
export function toRegistrationName(name) {
|
|
15
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fabricate the call value — one designation, two spellings:
|
|
19
|
+
* `callValueOf(Post, 'list', { query })` (class + verb) or
|
|
20
|
+
* `callValueOf({ entity, op }, input)` (raw, for dynamic bridges).
|
|
21
|
+
* Missing invocation fields complete to the empty invocation.
|
|
22
|
+
*/
|
|
23
|
+
export function callValueOf(target, opOrInput, input) {
|
|
24
|
+
const [call, given] = typeof opOrInput === 'string'
|
|
25
|
+
? [{ entity: toRegistrationName(target.name), op: opOrInput }, input]
|
|
26
|
+
: [target, opOrInput];
|
|
27
|
+
return { call, invocation: { ...EMPTY_INVOCATION, ...given } };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,gBAAgB,EAA0B,MAAM,iBAAiB,CAAC;AAG3E,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAcnD,oFAAoF;AACpF,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAoC,EACpC,SAA+C,EAC/C,KAAkC;IAElC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GACjB,OAAO,SAAS,KAAK,QAAQ;QAC3B,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,CAAE,MAA2B,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC;QAC3F,CAAC,CAAC,CAAC,MAAmB,EAAE,SAAS,CAAC,CAAC;IACvC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,GAAG,gBAAgB,EAAE,GAAG,KAAK,EAAE,EAAE,CAAC;AACjE,CAAC"}
|
package/dist/crud.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EntityOrm, ListOptions, ListResult } from './orm.js';
|
|
2
|
+
import type { OperationContract } from './operation.js';
|
|
3
|
+
/** Anything the derivation algebra produces — an entity class or one of its views. */
|
|
4
|
+
type SchemaCtor = abstract new (...args: any[]) => any;
|
|
5
|
+
/** The five ops the mixin fabricates. */
|
|
6
|
+
export type CrudOpName = 'list' | 'findById' | 'create' | 'update' | 'delete';
|
|
7
|
+
/** Which view each op speaks — omitted ops speak the entity, the trivial view. */
|
|
8
|
+
export type CrudViews = Partial<Record<CrudOpName, SchemaCtor>>;
|
|
9
|
+
/**
|
|
10
|
+
* The view an op emits, fabricated: the one declared for it, the single view when the
|
|
11
|
+
* whole handler declares one, the entity otherwise. `PostCard` is not a hand-written
|
|
12
|
+
* type — `Post.pick(...)` derives it field by field, so an op's return is a projection
|
|
13
|
+
* of the entity exactly like the entity is the projection that keeps everything.
|
|
14
|
+
*/
|
|
15
|
+
type OutOf<V, K extends CrudOpName, T> = [
|
|
16
|
+
V
|
|
17
|
+
] extends [SchemaCtor] ? InstanceType<V & SchemaCtor> : K extends keyof V ? (V[K] extends SchemaCtor ? InstanceType<V[K]> : T) : T;
|
|
18
|
+
/**
|
|
19
|
+
* The five ops, typed from the entity and its views.
|
|
20
|
+
*
|
|
21
|
+
* Two things the mixin declares but does not own. The **output** is the view the
|
|
22
|
+
* handler names (`Crud(Post, { list: PostCard })`) — fabricated, so a redefinition
|
|
23
|
+
* that returns cards stays assignable. The **trailing parameters** are resolved by
|
|
24
|
+
* type from the container (`delete(id, user: User | null)` gets its user from a
|
|
25
|
+
* collector): the mixin cannot know them, they belong to the app, so it declares that
|
|
26
|
+
* a tail exists and that it supplies none — which is what `never` says, and what keeps
|
|
27
|
+
* a judged redefinition assignable.
|
|
28
|
+
*/
|
|
29
|
+
export interface CrudOps<T, V = {}> {
|
|
30
|
+
orm: EntityOrm<T>;
|
|
31
|
+
list(options?: ListOptions, ...collected: never[]): Promise<ListResult<OutOf<V, 'list', T>>>;
|
|
32
|
+
findById(id: string, ...collected: never[]): Promise<OutOf<V, 'findById', T> | undefined>;
|
|
33
|
+
create(input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'create', T>>;
|
|
34
|
+
update(id: string, input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'update', T>>;
|
|
35
|
+
delete(id: string, ...collected: never[]): Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
/** The prefab handler class — its ops, plus the statics the bootstrap and adapters read. */
|
|
38
|
+
export interface CrudConstructor<T, V = {}> {
|
|
39
|
+
new (orm: EntityOrm<T>): CrudOps<T, V>;
|
|
40
|
+
readonly __entity: unknown;
|
|
41
|
+
readonly __output: unknown;
|
|
42
|
+
readonly __opOutputs?: CrudViews;
|
|
43
|
+
readonly __ops: Record<string, OperationContract>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mixin — extends Crud(Entity) to get all 5 typed CRUD methods.
|
|
47
|
+
*
|
|
48
|
+
* The second argument (optional) names the view the ops emit, and comes in two
|
|
49
|
+
* spellings of one idea — a view per op, or one view for all five:
|
|
50
|
+
*
|
|
51
|
+
* Crud(Post) → every op emits Post
|
|
52
|
+
* Crud(Post, { list: PostCard }) → list emits cards, the rest emit Post. Declaration
|
|
53
|
+
* only: the handler keeps its full-row ORM, so a
|
|
54
|
+
* judge can still read `body`.
|
|
55
|
+
* Crud(Post, PostPublic) → every op emits PostPublic, and the bootstrap
|
|
56
|
+
* scopes the injected ORM via .output(PostPublic) —
|
|
57
|
+
* the whole handler speaks the restricted view.
|
|
58
|
+
*/
|
|
59
|
+
export declare function Crud<E extends abstract new (...args: any[]) => any, V extends CrudViews | SchemaCtor = {}>(entity: E, output?: V): CrudConstructor<InstanceType<E>, V>;
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=crud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../src/crud.ts"],"names":[],"mappings":"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"}
|
package/dist/crud.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** The id of the row an op acts on — a route segment, or a query fallback. */
|
|
2
|
+
const byId = { name: 'id', source: { kind: 'param', name: 'id' }, optional: false };
|
|
3
|
+
const fromBody = { name: 'input', source: { kind: 'body' }, optional: false };
|
|
4
|
+
/**
|
|
5
|
+
* The five ops a Crud handler brings, declared rather than discovered.
|
|
6
|
+
*
|
|
7
|
+
* The mixin built them, so it alone knows their contract in full: what judges
|
|
8
|
+
* their input, where each argument comes from. It says so on the class, at
|
|
9
|
+
* runtime — which is what makes the guarantee independent of the AST scan (an
|
|
10
|
+
* installed app cannot resolve this file, and never needs to).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* `output` says the entity, and saying it costs nothing at runtime.
|
|
14
|
+
*
|
|
15
|
+
* The façade already projected onto the entity when nothing else was named, so this
|
|
16
|
+
* changes no result: `outputFieldsFor` reads `contractOutput` and falls back to the
|
|
17
|
+
* entity, and both are the same shape here. What changes is that the sentence now
|
|
18
|
+
* EXISTS — the identity card publishes `output` per op, and a card was measured
|
|
19
|
+
* carrying none at all (2026-08-06, ten ops, zero outputs), which typed every remote
|
|
20
|
+
* return as `unknown` for anyone building on it.
|
|
21
|
+
*
|
|
22
|
+
* It does not close the view: only an explicit `__opOutputs` does (`closed: perOp !==
|
|
23
|
+
* undefined`), so a named view still wins and a presenter's computed fields still ride
|
|
24
|
+
* out. `delete` names none — a boolean is not a shape.
|
|
25
|
+
*/
|
|
26
|
+
function crudOps(entity) {
|
|
27
|
+
return {
|
|
28
|
+
list: { output: entity, cardinality: 'page', binding: [{ name: 'options', source: { kind: 'query' }, optional: true }] },
|
|
29
|
+
findById: { output: entity, cardinality: 'maybe', binding: [byId] },
|
|
30
|
+
create: { input: entity, output: entity, cardinality: 'one', binding: [fromBody] },
|
|
31
|
+
// The patch view carries its own mode: an absent field is untouched, an
|
|
32
|
+
// immutable one re-supplied is refused.
|
|
33
|
+
update: { input: entity.partial?.(), output: entity, cardinality: 'one', binding: [byId, fromBody] },
|
|
34
|
+
delete: { cardinality: 'none', binding: [byId] },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The mixin's single "trust me" point — the twin of `asSchemaConstructor` in @fougere/schema.
|
|
39
|
+
*
|
|
40
|
+
* The implementation returns whatever the ORM hands back; the declaration names the view
|
|
41
|
+
* each op emits at the port. TypeScript cannot connect the two (the view is a runtime
|
|
42
|
+
* argument, the type is a generic), so one assertion states that the mixin honours what it
|
|
43
|
+
* declared — and the façade makes it true, projecting each op's result onto its view
|
|
44
|
+
* (`outputFieldsFor` in `bootstrap.ts`).
|
|
45
|
+
*/
|
|
46
|
+
function asCrudConstructor(impl) {
|
|
47
|
+
return impl;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mixin — extends Crud(Entity) to get all 5 typed CRUD methods.
|
|
51
|
+
*
|
|
52
|
+
* The second argument (optional) names the view the ops emit, and comes in two
|
|
53
|
+
* spellings of one idea — a view per op, or one view for all five:
|
|
54
|
+
*
|
|
55
|
+
* Crud(Post) → every op emits Post
|
|
56
|
+
* Crud(Post, { list: PostCard }) → list emits cards, the rest emit Post. Declaration
|
|
57
|
+
* only: the handler keeps its full-row ORM, so a
|
|
58
|
+
* judge can still read `body`.
|
|
59
|
+
* Crud(Post, PostPublic) → every op emits PostPublic, and the bootstrap
|
|
60
|
+
* scopes the injected ORM via .output(PostPublic) —
|
|
61
|
+
* the whole handler speaks the restricted view.
|
|
62
|
+
*/
|
|
63
|
+
export function Crud(entity, output) {
|
|
64
|
+
// A view is a class (it carries fields) ; a map of views is a plain object.
|
|
65
|
+
const perOp = typeof output === 'object' && output !== null ? output : undefined;
|
|
66
|
+
const wholeHandler = typeof output === 'function' ? output : undefined;
|
|
67
|
+
return asCrudConstructor(class CrudHandler {
|
|
68
|
+
static __entity = entity;
|
|
69
|
+
/** Handler-wide view only — a per-op map must NOT scope the ORM the judges read. */
|
|
70
|
+
static __output = wholeHandler ?? entity;
|
|
71
|
+
static __opOutputs = perOp;
|
|
72
|
+
/** What this prefab handler declares — read by the façade, merged under the author's own methods. */
|
|
73
|
+
static __ops = crudOps(entity);
|
|
74
|
+
orm;
|
|
75
|
+
constructor(orm) {
|
|
76
|
+
this.orm = orm;
|
|
77
|
+
}
|
|
78
|
+
async list(options) { return this.orm.list(options); }
|
|
79
|
+
async findById(id) { return this.orm.findById(id); }
|
|
80
|
+
async create(input) { return this.orm.create(input); }
|
|
81
|
+
async update(id, input) { return this.orm.update(id, input); }
|
|
82
|
+
async delete(id) { return this.orm.delete(id); }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=crud.js.map
|
package/dist/crud.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crud.js","sourceRoot":"","sources":["../src/crud.ts"],"names":[],"mappings":"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"}
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FougereConfig } from './config-loader.js';
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper for fougere.config.ts — preserves literal types for autocomplete
|
|
4
|
+
* while validating against FougereConfig. Equivalent to `satisfies FougereConfig`
|
|
5
|
+
* but more explicit and aligned with defineFrond().
|
|
6
|
+
*/
|
|
7
|
+
export declare function defineFougere<T extends FougereConfig>(config: T): T;
|
|
8
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEnE"}
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity helper for fougere.config.ts — preserves literal types for autocomplete
|
|
3
|
+
* while validating against FougereConfig. Equivalent to `satisfies FougereConfig`
|
|
4
|
+
* but more explicit and aligned with defineFrond().
|
|
5
|
+
*/
|
|
6
|
+
export function defineFougere(config) {
|
|
7
|
+
return config;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAA0B,MAAS;IAC9D,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/egress.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value leaving the domain.
|
|
3
|
+
*
|
|
4
|
+
* Two things happen every time, in this order:
|
|
5
|
+
*
|
|
6
|
+
* 1. JUDGE — is this a legal value? Read from `shape` alone, so the answer does not
|
|
7
|
+
* depend on who is receiving. It has to come first: judging after the projection
|
|
8
|
+
* would fail on a client view that legitimately dropped its `writeOnly` fields.
|
|
9
|
+
* 2. PROJECT — what may THIS receiver see? A client does not get a password hash;
|
|
10
|
+
* storage sees everything.
|
|
11
|
+
*
|
|
12
|
+
* Storage is a way out like the others. It used to be the exception — the ORM wrote
|
|
13
|
+
* whatever a handler handed it, so `status: 'n-importe-quoi'` on a
|
|
14
|
+
* `oneOf('draft','published')` was stored and read back unchanged (measured
|
|
15
|
+
* 2026-07-25). The database is a weak judge: it catches nullability and, outside
|
|
16
|
+
* SQLite, the column type — never a closed set, a format or a range, since the DDL
|
|
17
|
+
* emits no CHECK. The choice was never "fail late or judge early", it was "corrupt
|
|
18
|
+
* silently or judge early".
|
|
19
|
+
*
|
|
20
|
+
* What the judge must NOT read: `boundary` and `lifecycle`. Both answer "may a CLIENT
|
|
21
|
+
* send this?", a question with no meaning when the domain itself is writing — reading
|
|
22
|
+
* them here is exactly what makes `Post.validate(a_db_row)` come back invalid.
|
|
23
|
+
*
|
|
24
|
+
* Shallow, like `encodeFields` it stands on: a relation's nested rows are not reached.
|
|
25
|
+
* A handler that hand-rolls its own envelope owns its own egress.
|
|
26
|
+
*/
|
|
27
|
+
import { type Fields } from '@fougere/schema';
|
|
28
|
+
/**
|
|
29
|
+
* Refuse a value the shape does not accept, before it goes anywhere.
|
|
30
|
+
*
|
|
31
|
+
* Only the keys actually present are judged, so a patch stays a patch: an update
|
|
32
|
+
* naming one field says nothing about the others. A key with no field is not this
|
|
33
|
+
* judge's business — the receiver fails on its own (an unknown column), and a client
|
|
34
|
+
* input already met `validateFields`, which refuses strangers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function judgeEgress(fields: Fields, value: unknown, entity: string, operation: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Drop what this receiver may not see — a `writeOnly` field goes here — then hand the
|
|
39
|
+
* rest to `encodeFields`, which converts the values to their wire form.
|
|
40
|
+
*
|
|
41
|
+
* `closed` says the field set is the WHOLE of what this op emits, so anything else is
|
|
42
|
+
* dropped. That is what naming a view for an op means (`Crud(Post, { list: PostCard })`):
|
|
43
|
+
* the author states the audience, and a field they left out must not ride along. Open is
|
|
44
|
+
* the default and stays the rule for the entity itself — a presenter's computed field is
|
|
45
|
+
* an addition to the entity's output, not an intruder.
|
|
46
|
+
*/
|
|
47
|
+
export declare function projectEgress(fields: Fields, result: unknown, closed?: boolean): unknown;
|
|
48
|
+
/**
|
|
49
|
+
* Add a presenter's computed fields — the last step out, after the projection.
|
|
50
|
+
*
|
|
51
|
+
* After, not before, for two reasons: a computed field is not an entity field, so
|
|
52
|
+
* `encodeFields` has no boundary to apply to it; and it must not be dropped by the
|
|
53
|
+
* projection, which keeps only what the entity declares.
|
|
54
|
+
*
|
|
55
|
+
* Skipped when the op names a view (`closed`): there the author stated exactly what
|
|
56
|
+
* this audience gets, and an addition they did not list would contradict it. Open is
|
|
57
|
+
* the default, and there a computed field is an addition to the entity's output, not
|
|
58
|
+
* an intruder.
|
|
59
|
+
*
|
|
60
|
+
* It ran only in the REST and GraphQL projections before, each with its own copy — so
|
|
61
|
+
* the same presenter answered on two doors and was silently absent from `useQuery`.
|
|
62
|
+
* The cost travels with it, which is why the signature takes the page: a field is handed
|
|
63
|
+
* every row at once and answers one value per row, so a read it needs is issued once.
|
|
64
|
+
*/
|
|
65
|
+
/** Resolved arguments per computed field — everything the signature declares after the rows. */
|
|
66
|
+
export type PresenterArgs = Record<string, unknown[]>;
|
|
67
|
+
export declare function presentEgress(result: unknown, presenter: Record<string, unknown> | undefined, fieldNames: string[] | undefined, entity?: string, operation?: string, args?: PresenterArgs): Promise<unknown>;
|
|
68
|
+
/**
|
|
69
|
+
* The storage way out: judge, then hand over. There is no projection step — storage is
|
|
70
|
+
* the receiver that sees everything, which is precisely why a `writeOnly` field can be
|
|
71
|
+
* persisted while never reaching a browser.
|
|
72
|
+
*
|
|
73
|
+
* `Object.create` keeps the original on the prototype chain, so reads, `output()` and
|
|
74
|
+
* whatever else an adapter carries still resolve, and a copy scoped later inherits the
|
|
75
|
+
* judgement instead of escaping it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function guardStorage<T extends object>(orm: T, fields: Fields, entityName: string): T;
|
|
78
|
+
//# sourceMappingURL=egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.d.ts","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAMxE;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAsBnG;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAQ,GAAG,OAAO,CAgBtF;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,gGAAgG;AAChG,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;AAEtD,wBAAsB,aAAa,CACjC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC9C,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAChC,MAAM,SAAY,EAClB,SAAS,SAAY,EACrB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,OAAO,CAAC,CA0ClB;AAUD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,CAgC5F"}
|