@fougere/core 0.3.0-alpha.0 → 0.4.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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/dist/boot/HandlerFacade.js +2 -2
  3. package/dist/boot/HandlerFacade.js.map +1 -1
  4. package/dist/boot/apply.d.ts.map +1 -1
  5. package/dist/boot/apply.js +2 -12
  6. package/dist/boot/apply.js.map +1 -1
  7. package/dist/boot/binding.js +4 -4
  8. package/dist/boot/binding.js.map +1 -1
  9. package/dist/boot/boot.d.ts +1 -1
  10. package/dist/boot/boot.d.ts.map +1 -1
  11. package/dist/boot/bootstrap.d.ts.map +1 -1
  12. package/dist/boot/bootstrap.js +14 -13
  13. package/dist/boot/bootstrap.js.map +1 -1
  14. package/dist/boot/frame.d.ts +1 -1
  15. package/dist/boot/frame.d.ts.map +1 -1
  16. package/dist/boot/frame.js +5 -14
  17. package/dist/boot/frame.js.map +1 -1
  18. package/dist/boot/ownership.d.ts.map +1 -1
  19. package/dist/boot/ownership.js +5 -5
  20. package/dist/boot/ownership.js.map +1 -1
  21. package/dist/boot/remote.d.ts.map +1 -1
  22. package/dist/boot/remote.js +2 -2
  23. package/dist/boot/remote.js.map +1 -1
  24. package/dist/boot/together.d.ts.map +1 -1
  25. package/dist/boot/together.js +6 -6
  26. package/dist/boot/together.js.map +1 -1
  27. package/dist/boot/types.d.ts +2 -2
  28. package/dist/boot/types.d.ts.map +1 -1
  29. package/dist/contract.d.ts +1 -1
  30. package/dist/contract.d.ts.map +1 -1
  31. package/dist/contract.js +3 -3
  32. package/dist/contract.js.map +1 -1
  33. package/dist/dispatch/InFlight.d.ts.map +1 -1
  34. package/dist/dispatch/InFlight.js.map +1 -1
  35. package/dist/dispatch/InputValidator.d.ts.map +1 -1
  36. package/dist/dispatch/InputValidator.js +3 -2
  37. package/dist/dispatch/InputValidator.js.map +1 -1
  38. package/dist/dispatch/OutputProjector.js +2 -2
  39. package/dist/dispatch/OutputProjector.js.map +1 -1
  40. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  41. package/dist/dispatch/StorageGuard.js +2 -2
  42. package/dist/dispatch/StorageGuard.js.map +1 -1
  43. package/dist/effective-operation.js +4 -5
  44. package/dist/effective-operation.js.map +1 -1
  45. package/dist/emit.d.ts +2 -2
  46. package/dist/emit.d.ts.map +1 -1
  47. package/dist/emit.js +2 -2
  48. package/dist/emit.js.map +1 -1
  49. package/dist/identity.js +1 -1
  50. package/dist/identity.js.map +1 -1
  51. package/dist/imports.d.ts +1 -1
  52. package/dist/imports.d.ts.map +1 -1
  53. package/dist/imports.js.map +1 -1
  54. package/dist/orm.d.ts.map +1 -1
  55. package/dist/orm.js +4 -4
  56. package/dist/orm.js.map +1 -1
  57. package/dist/prefab/collector.js +2 -2
  58. package/dist/prefab/collector.js.map +1 -1
  59. package/dist/prefab/mirror.d.ts.map +1 -1
  60. package/dist/prefab/mirror.js +2 -2
  61. package/dist/prefab/mirror.js.map +1 -1
  62. package/dist/prefab/presenter.d.ts.map +1 -1
  63. package/dist/prefab/presenter.js +2 -2
  64. package/dist/prefab/presenter.js.map +1 -1
  65. package/dist/prefab/repository.d.ts.map +1 -1
  66. package/dist/prefab/repository.js +2 -2
  67. package/dist/prefab/repository.js.map +1 -1
  68. package/dist/scan/emit.d.ts.map +1 -1
  69. package/dist/scan/emit.js +3 -2
  70. package/dist/scan/emit.js.map +1 -1
  71. package/dist/scan/handler-parser.d.ts +2 -0
  72. package/dist/scan/handler-parser.d.ts.map +1 -1
  73. package/dist/scan/handler-parser.js +52 -12
  74. package/dist/scan/handler-parser.js.map +1 -1
  75. package/dist/scan/scanner.d.ts.map +1 -1
  76. package/dist/scan/scanner.js +37 -10
  77. package/dist/scan/scanner.js.map +1 -1
  78. package/dist/verify.js +2 -2
  79. package/dist/verify.js.map +1 -1
  80. package/dist/wire/call.d.ts +7 -7
  81. package/dist/wire/call.d.ts.map +1 -1
  82. package/dist/wire/call.js +6 -6
  83. package/dist/wire/call.js.map +1 -1
  84. package/package.json +6 -4
  85. package/src/boot/AppLifecycle.ts +50 -0
  86. package/src/boot/Emissions.ts +263 -0
  87. package/src/boot/HandlerFacade.ts +242 -0
  88. package/src/boot/Lifecycle.ts +3 -0
  89. package/src/boot/ambient-port.ts +41 -0
  90. package/src/boot/ambient.als.ts +43 -0
  91. package/src/boot/ambient.queue.ts +75 -0
  92. package/src/boot/apply.ts +47 -0
  93. package/src/boot/auth.ts +55 -0
  94. package/src/boot/binding.ts +182 -0
  95. package/src/boot/boot.ts +125 -0
  96. package/src/boot/bootstrap.ts +839 -0
  97. package/src/boot/frame.ts +210 -0
  98. package/src/boot/ownership.ts +140 -0
  99. package/src/boot/ports.ts +66 -0
  100. package/src/boot/remote.ts +179 -0
  101. package/src/boot/seed.ts +158 -0
  102. package/src/boot/together.ts +215 -0
  103. package/src/boot/types.ts +261 -0
  104. package/src/builtins/config.ts +19 -0
  105. package/src/builtins/logger.ts +175 -0
  106. package/src/config-loader.ts +146 -0
  107. package/src/contract/Call.ts +18 -0
  108. package/src/contract/Invocation.ts +72 -0
  109. package/src/contract/RouteAddress.ts +49 -0
  110. package/src/contract.ts +74 -0
  111. package/src/crypto/encoding.ts +59 -0
  112. package/src/crypto/node.ts +30 -0
  113. package/src/crypto/port.ts +45 -0
  114. package/src/crypto/webcrypto.ts +55 -0
  115. package/src/define.ts +10 -0
  116. package/src/dispatch/ArgumentResolver.ts +17 -0
  117. package/src/dispatch/ArrayResult.ts +10 -0
  118. package/src/dispatch/DispatchEvent.ts +38 -0
  119. package/src/dispatch/DispatchLifecycle.ts +23 -0
  120. package/src/dispatch/DispatchPort.ts +6 -0
  121. package/src/dispatch/Dispatcher.ts +48 -0
  122. package/src/dispatch/InFlight.ts +49 -0
  123. package/src/dispatch/InputValidator.ts +28 -0
  124. package/src/dispatch/LocalRoutePolicy.ts +34 -0
  125. package/src/dispatch/OperationExecutor.ts +60 -0
  126. package/src/dispatch/OperationRoute.ts +18 -0
  127. package/src/dispatch/OutputProjector.ts +32 -0
  128. package/src/dispatch/OutputView.ts +9 -0
  129. package/src/dispatch/PresenterArgumentResolver.ts +25 -0
  130. package/src/dispatch/PresenterExecutor.ts +55 -0
  131. package/src/dispatch/RemoteRouteResolver.ts +21 -0
  132. package/src/dispatch/Route.ts +11 -0
  133. package/src/dispatch/RouteNotFoundError.ts +29 -0
  134. package/src/dispatch/RoutePolicy.ts +8 -0
  135. package/src/dispatch/RouteRegistry.ts +73 -0
  136. package/src/dispatch/RouteResolver.ts +7 -0
  137. package/src/dispatch/StorageGuard.ts +71 -0
  138. package/src/effective-operation.ts +597 -0
  139. package/src/emit.ts +90 -0
  140. package/src/entry/DynamicFacade.ts +28 -0
  141. package/src/entry/FacadeEntry.ts +32 -0
  142. package/src/entry/TransportEntry.ts +30 -0
  143. package/src/frond-config.ts +138 -0
  144. package/src/graph.ts +180 -0
  145. package/src/identity-keys.ts +43 -0
  146. package/src/identity.ts +235 -0
  147. package/src/imports.ts +123 -0
  148. package/src/index.ts +106 -0
  149. package/src/loader.ts +41 -0
  150. package/src/node.ts +28 -0
  151. package/src/orm.ts +252 -0
  152. package/src/prefab/collector.ts +36 -0
  153. package/src/prefab/crud.ts +188 -0
  154. package/src/prefab/mirror.ts +122 -0
  155. package/src/prefab/prefab.ts +46 -0
  156. package/src/prefab/presenter.ts +63 -0
  157. package/src/prefab/repository.ts +170 -0
  158. package/src/scan/Fronds.ts +73 -0
  159. package/src/scan/conventions.ts +122 -0
  160. package/src/scan/emit.ts +231 -0
  161. package/src/scan/frond.ts +235 -0
  162. package/src/scan/handler-parser.ts +818 -0
  163. package/src/scan/scan-cache.ts +115 -0
  164. package/src/scan/scanner.ts +758 -0
  165. package/src/verify.ts +193 -0
  166. package/src/wire/call.ts +369 -0
  167. package/src/wire/errors.ts +124 -0
  168. package/src/wire/http-error.ts +77 -0
  169. package/src/wire/invocation.ts +10 -0
  170. package/src/wire/loggerMiddleware.ts +29 -0
  171. package/src/wire/middleware.ts +47 -0
  172. package/src/wire/operation.ts +258 -0
  173. package/dist/boot/egress.d.ts +0 -8
  174. package/dist/boot/egress.d.ts.map +0 -1
  175. package/dist/boot/egress.js +0 -11
  176. package/dist/boot/egress.js.map +0 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Where each parameter of an operation gets its value — decided once at boot from the
3
+ * parsed signature, replayed per call by `resolveArgs`.
4
+ *
5
+ * The branches are ORDERED and each states why it sits where it does; the list used to
6
+ * be repeated here too, and it had already lost `Fact` — the one whose position is
7
+ * load-bearing, since the fall-through would otherwise hand it the caller's body.
8
+ */
9
+ import type { ParsedParam } from '../scan/handler-parser.js';
10
+ import type { InvocationContext } from '../wire/invocation.js';
11
+ import { lowerFirst } from '@fougere/schema';
12
+
13
+ // ── Types ─────────────────────────────────────
14
+
15
+ type ParamSource =
16
+ | { kind: 'collector'; typeName: string }
17
+ /**
18
+ * `Fact<PostPublished>` — something that happened, not something a caller typed.
19
+ *
20
+ * It resolves from the body exactly like `body` does, and that is deliberate: an
21
+ * emission and a direct call ARE the same call, so a subscriber cannot tell them apart
22
+ * and does not need to. The branch exists so the PLAN says what this parameter is —
23
+ * that sentence is what makes the subscriber index readable without reparsing types.
24
+ */
25
+ | { kind: 'fact'; factName: string }
26
+ | { kind: 'param'; name: string; coerce?: 'number' | 'boolean' }
27
+ | { kind: 'body' }
28
+ | { kind: 'context' }
29
+ /** The whole query bag, for an op whose argument IS the options (list). */
30
+ | { kind: 'query' };
31
+
32
+ interface ParamBinding {
33
+ name: string;
34
+ source: ParamSource;
35
+ optional: boolean;
36
+ }
37
+
38
+ export type BindingPlan = ParamBinding[];
39
+
40
+ // ── Primitives ────────────────────────────────
41
+
42
+ const PRIMITIVES = new Set(['string', 'number', 'boolean']);
43
+
44
+ function coercionFor(typeName: string): 'number' | 'boolean' | undefined {
45
+ if (typeName === 'number') return 'number';
46
+ if (typeName === 'boolean') return 'boolean';
47
+ return undefined;
48
+ }
49
+
50
+ // ── Compute ───────────────────────────────────
51
+
52
+ /**
53
+ * Build a BindingPlan from parsed method params.
54
+ *
55
+ * @param params - Parsed parameter list from AST
56
+ * @param collectorTypeNames - Registration keys of the types a Collector answers for
57
+ */
58
+ export function computeBindingPlan(
59
+ params: ParsedParam[],
60
+ collectorTypeNames: Set<string>,
61
+ ): BindingPlan {
62
+ return params.map((param) => {
63
+ const typeName = param.type.name;
64
+ // `lowerFirst`, never `toLowerCase()`: the collector set is keyed the way the
65
+ // scan spells it, and the two agree on one word only — `AuthorUser` looked up as
66
+ // `authoruser` missed `authorUser` and fell through to branch 4, the request body.
67
+ const typeKey = lowerFirst(typeName);
68
+
69
+ // 0. Fact — `Fact<X>` names itself, so nothing has to be known in advance. It comes
70
+ // FIRST because branch 4 would otherwise hand it the caller's body under the name
71
+ // of something that happened.
72
+ const factOf = param.type.name === 'Fact' ? param.type.generics?.[0]?.name : undefined;
73
+ if (factOf) {
74
+ return {
75
+ name: param.name,
76
+ source: { kind: 'fact' as const, factName: lowerFirst(factOf) },
77
+ optional: param.optional ?? false,
78
+ };
79
+ }
80
+
81
+ // 1. Collector — param type matches a type some collector answers for
82
+ if (collectorTypeNames.has(typeKey)) {
83
+ return {
84
+ name: param.name,
85
+ source: { kind: 'collector' as const, typeName: typeKey },
86
+ optional: param.optional ?? false,
87
+ };
88
+ }
89
+
90
+ // 2. InvocationContext — inject the full context
91
+ if (typeName === 'InvocationContext') {
92
+ return {
93
+ name: param.name,
94
+ source: { kind: 'context' as const },
95
+ optional: param.optional ?? false,
96
+ };
97
+ }
98
+
99
+ // 3. Primitives — matched by name from params > query
100
+ if (PRIMITIVES.has(typeName)) {
101
+ return {
102
+ name: param.name,
103
+ source: {
104
+ kind: 'param' as const,
105
+ name: param.name,
106
+ coerce: coercionFor(typeName),
107
+ },
108
+ optional: param.optional ?? false,
109
+ };
110
+ }
111
+
112
+ // 4. Everything else — body
113
+ return {
114
+ name: param.name,
115
+ source: { kind: 'body' as const },
116
+ optional: param.optional ?? false,
117
+ };
118
+ });
119
+ }
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
+ }
@@ -0,0 +1,125 @@
1
+ import { createApp } from './bootstrap.js';
2
+ import { seeding } from './seed.js';
3
+ import { scanProject } from '../scan/scanner.js';
4
+ import { loadConfig, type FougereConfig } from '../config-loader.js';
5
+ import { Logger } from '../builtins/logger.js';
6
+ import { applyConfig } from './apply.js';
7
+ import type { App, CreateAppOptions } from './types.js';
8
+ import { migrating, type Extension } from './AppLifecycle.js';
9
+ import type { Transport } from '../wire/call.js';
10
+ import type { Container } from '@fougere/container';
11
+
12
+ interface BootOptions {
13
+ /** Project root. Defaults to process.cwd(). */
14
+ root?: string;
15
+ /** Override config (merged with fougere.config.ts). */
16
+ config?: Partial<FougereConfig>;
17
+ /** Container factory. Required. */
18
+ createContainer: () => Container;
19
+ /** Only boot these fronds (by name). Absent = all. */
20
+ fronds?: string[];
21
+ /**
22
+ * Remote fronds — label → address. A declared remote wins over local
23
+ * presence (the frond runs elsewhere). Requires `remoteTransport`.
24
+ */
25
+ remotes?: Record<string, string>;
26
+ /** Builds the transport to reach `remotes`. Supplied by a layer-2 package. */
27
+ remoteTransport?: (url: string) => Transport;
28
+ /**
29
+ * Carries an announced fact out of this process. Forwarded rather than dropped: a
30
+ * caller handing one in has no other way to reach the emitter, and this path silently
31
+ * kept it to itself.
32
+ */
33
+ onEmit?: CreateAppOptions['onEmit'];
34
+ /**
35
+ * ORM setup — returns the storage handle (db), an ormFactory, and its two halves.
36
+ * The `db` value is forwarded to the auth provider via AuthContext when `auth` is set.
37
+ */
38
+ db?: (config: FougereConfig) => {
39
+ db?: unknown;
40
+ ormFactory: CreateAppOptions['ormFactory'];
41
+ /**
42
+ * Bring the schema up to date — an extension's `up`, because it runs after the
43
+ * container. It used to be called `afterBoot`, a word that also meant the host's
44
+ * post-boot and was read in four places under two senses.
45
+ */
46
+ migrate?: (app: App) => Promise<void> | void;
47
+ /** Closes what the factory opened — `boot()` called it, so `boot()` releases it. */
48
+ close?: () => Promise<void>;
49
+ };
50
+ /**
51
+ * What this app takes on beyond its fronds. Appended after the framework's own, so a
52
+ * host adds to the ascent — or replaces a member of it by declaring the same name.
53
+ */
54
+ extensions?: readonly (Extension | undefined)[];
55
+ }
56
+
57
+ /**
58
+ * Boot a Fougere app from fougere.config.ts.
59
+ *
60
+ * Handles: config loading, container creation, DB setup, seeding.
61
+ * Works for any surface (Nuxt, GraphQL standalone, CLI, tests).
62
+ */
63
+ export async function boot(options: BootOptions): Promise<App> {
64
+ const bootStart = performance.now();
65
+ const log = new Logger('boot');
66
+
67
+ const root = options.root ?? process.cwd();
68
+ log.info(`root: ${root}`);
69
+
70
+ log.debug('loading config');
71
+ const fileConfig = await loadConfig(root);
72
+ const config: FougereConfig = { ...fileConfig, ...options.config };
73
+ // What this config changes in the process, said the same way at boot and at reload —
74
+ // there is one applier, and a host re-reading later calls the very same function.
75
+ applyConfig(config);
76
+ log.info('config loaded');
77
+
78
+ let dbSetup: ReturnType<NonNullable<BootOptions['db']>> | undefined;
79
+ if (options.db) {
80
+ log.debug('initializing database');
81
+ dbSetup = options.db(config);
82
+ log.info('database initialized');
83
+ }
84
+
85
+ log.debug('creating app (scan + container)');
86
+ const app = await createApp({
87
+ // boot() lives on the Node entry, so boot() is what reads the disk. `createApp` is
88
+ // handed the answer and reaches for nothing.
89
+ scan: await scanProject(root, options.fronds, config.conventions),
90
+ createContainer: options.createContainer,
91
+ ormFactory: dbSetup?.ormFactory,
92
+ db: dbSetup?.db,
93
+ auth: config.auth,
94
+ remotes: options.remotes,
95
+ ports: config.ports,
96
+ // Read from the config for the same reason `ports` is, one line up: it is a fact the
97
+ // project states, not one the caller passes. Absent here, `serveRest` and
98
+ // `serveGraphQL` both answered `pass` on an app whose config declared them — the
99
+ // hosts got it right through their own boot (`app/shared/src/boot.ts`), and this
100
+ // path, the conventional one, served nothing.
101
+ adapters: config.adapters,
102
+ // boot() called the factory, so boot() owns closing what it opened. Not an extension:
103
+ // it was opened before the container existed, so it closes after the container goes.
104
+ onDispose: dbSetup?.close,
105
+ /**
106
+ * The whole ascent, in one ordered list — tables, then rows, then whatever the host
107
+ * takes on. It was four call sites under the name `afterBoot`, two of which meant
108
+ * different things and one of which was generated into a Nitro plugin.
109
+ */
110
+ extensions: [
111
+ migrating(dbSetup?.migrate),
112
+ seeding((message) => log.debug(message)),
113
+ ...(options.extensions ?? []),
114
+ ],
115
+ onEmit: options.onEmit,
116
+ remoteTransport: options.remoteTransport,
117
+ });
118
+
119
+ log.info(`ascent: ${app.extensions().join(' → ') || 'nothing declared'}`);
120
+
121
+ const ms = (performance.now() - bootStart).toFixed(0);
122
+ log.info(`ready in ${ms}ms — ${app.fronds.length} frond(s)`);
123
+
124
+ return app;
125
+ }