@fougere/defaults 0.3.0-alpha.0 → 0.5.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/README.md CHANGED
@@ -12,4 +12,4 @@ pnpm add @fougere/defaults
12
12
 
13
13
  Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
14
14
  monolith to distributed, the same user code.
15
- Reference documentation: [the site](https://chok.github.io/fougere/) (en/fr).
15
+ Reference documentation: [the site](https://fougere.dev/) (en/fr).
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAW,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG7E,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAChB,KAAK,GACL,QAAQ,GACR;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACnC,SAAS,CAAC;AAEd,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC;AAEhH,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IAC7D;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACnH;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7C;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAClC,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAGzD;AAwDD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAazF;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,EAAE,EAAE,KAAK,CAAC;IACV,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CAgEtE"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAW,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG7E,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAChB,KAAK,GACL,QAAQ,GACR;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACnC,SAAS,CAAC;AAEd,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC;AAEhH,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IAC7D;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACnH;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7C;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAClC,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAGzD;AAqDD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAazF;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,EAAE,EAAE,KAAK,CAAC;IACV,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CA6DtE"}
package/dist/storage.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Fronds } from '@fougere/core';
2
- import { registrationKeyOf } from '@fougere/core/contract';
2
+ import { lowerFirst } from '@fougere/core/contract';
3
3
  import { migrate } from '@fougere/adapter-sql';
4
4
  import { setupSqlite } from '@fougere/adapter-sql/sqlite';
5
5
  /** Does this config ask for persistence at all? */
@@ -36,7 +36,7 @@ function refuseUnresolvable(declared, field) {
36
36
  * Auth entities ride with the default source: a provider's tables are the app's own,
37
37
  * and nothing yet lets one declare where it lives.
38
38
  */
39
- function partition(app, holds, withAuth, materialize) {
39
+ function partition(app, holds, withAuth) {
40
40
  const typed = app;
41
41
  const fronds = typed.fronds
42
42
  .map((frond) => ({ ...frond, entities: frond.entities.filter((entry) => holds(entry.name)) }))
@@ -47,8 +47,6 @@ function partition(app, holds, withAuth, materialize) {
47
47
  // Lifted, because this function reads its app structurally on purpose — a caller
48
48
  // may hand it a shape that is app-LIKE, and the question is still the same one.
49
49
  elsewhere: Fronds.scanned(typed.fronds).entityNames().filter((name) => !holds(name)),
50
- // A derivation makes no table unless a source names it — see AppLike.materialize.
51
- materialize,
52
50
  };
53
51
  }
54
52
  /**
@@ -101,27 +99,24 @@ export function storageFrom(declared) {
101
99
  // registration ran last — the same silent duplicate `remotes:` refuses one level up.
102
100
  const home = new Map();
103
101
  const engines = new Map();
104
- /** Everything a source names — the opt-in that turns a derivation into stored rows. */
105
- const named = [];
106
102
  for (const [name, placement] of Object.entries(sources ?? {})) {
107
103
  engines.set(name, placement.setup);
108
104
  for (const entity of placement.entities) {
109
- const key = registrationKeyOf(entity);
105
+ const key = lowerFirst(entity);
110
106
  const claimed = home.get(key);
111
107
  if (claimed) {
112
108
  throw new Error(`sources: '${entity}' is claimed by both '${claimed}' and '${name}' — an entity lives in one place.`);
113
109
  }
114
110
  home.set(key, name);
115
- named.push(entity);
116
111
  }
117
112
  }
118
113
  // The default source answers to the name the config gave it, so a refusal naming two
119
114
  // sources names things the author can find in their own file.
120
115
  const DEFAULT = 'db';
121
- const sourceOf = (entityName) => home.get(registrationKeyOf(entityName)) ?? DEFAULT;
116
+ const sourceOf = (entityName) => home.get(lowerFirst(entityName)) ?? DEFAULT;
122
117
  const engineOf = (source) => (source === DEFAULT ? base : engines.get(source));
123
118
  const engineFor = (entityName) => {
124
- const source = home.get(registrationKeyOf(entityName));
119
+ const source = home.get(lowerFirst(entityName));
125
120
  return (source && engines.get(source)) || base;
126
121
  };
127
122
  return {
@@ -142,9 +137,9 @@ export function storageFrom(declared) {
142
137
  // One pass per source, each seeing only its own tables — which is what makes a
143
138
  // cross-source `ref()` a miss rather than a constraint against a stranger.
144
139
  migrate: async (app) => {
145
- await migrate(partition(app, (name) => !home.has(registrationKeyOf(name)), true, named), base.db);
140
+ await migrate(partition(app, (name) => !home.has(lowerFirst(name)), true), base.db);
146
141
  for (const [name, engine] of engines) {
147
- await migrate(partition(app, (e) => home.get(registrationKeyOf(e)) === name, false, named), engine.db);
142
+ await migrate(partition(app, (e) => home.get(lowerFirst(e)) === name, false), engine.db);
148
143
  }
149
144
  },
150
145
  // Every engine, the default one last: a named source may hold what the default
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/defaults",
3
- "version": "0.3.0-alpha.0",
3
+ "version": "0.5.0-alpha.0",
4
4
  "description": "The conventional boot of a Fougere app — the one place storage is wired.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -25,13 +25,14 @@
25
25
  }
26
26
  },
27
27
  "files": [
28
- "dist"
28
+ "dist",
29
+ "src"
29
30
  ],
30
31
  "dependencies": {
31
- "@fougere/core": "0.3.0-alpha.0",
32
- "@fougere/transport-http": "0.3.0-alpha.0",
33
- "@fougere/adapter-sql": "0.3.0-alpha.0",
34
- "@fougere/container": "0.3.0-alpha.0"
32
+ "@fougere/core": "0.5.0-alpha.0",
33
+ "@fougere/transport-http": "0.5.0-alpha.0",
34
+ "@fougere/adapter-sql": "0.5.0-alpha.0",
35
+ "@fougere/container": "0.5.0-alpha.0"
35
36
  },
36
37
  "publishConfig": {
37
38
  "access": "public"
@@ -40,7 +41,7 @@
40
41
  "kysely": "^0.28.17",
41
42
  "better-sqlite3": "^13.0.3",
42
43
  "@types/better-sqlite3": "^7.6.13",
43
- "@fougere/schema": "0.3.0-alpha.0"
44
+ "@fougere/schema": "0.5.0-alpha.0"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "rm -rf dist && tsc",
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fougere/defaults — the conventional boot, declared once.
3
+ *
4
+ * Booting an app from `fougere.config.ts` means the same three opinionated
5
+ * bindings every time: the container is `container`, `db: sqlite`
6
+ * realizes through `schema-sql`, and `remotes:` are reached over
7
+ * `transport-http`. core stays pure (it knows none of these); this layer-2
8
+ * package supplies them on top of `core.boot()`. Nuxt's fallback, the CLI's
9
+ * `serve`/`call`, and a standalone frond host are all projections of it.
10
+ */
11
+ import { type App, type CreateAppOptions } from '@fougere/core';
12
+ import { boot, loadConfig } from '@fougere/core/node';
13
+ import { createContainer } from '@fougere/container';
14
+ import { createHttpTransport } from '@fougere/transport-http';
15
+ import { resolveStorage, type DbConfig } from './storage.js';
16
+
17
+ export { resolveStorage, declaresStorage, storageFrom } from './storage.js';
18
+ export type { DbConfig, SourcesConfig, DeclaredStorage, Placement, ResolvedStorage } from './storage.js';
19
+
20
+ export interface BootAppOptions {
21
+ /** Boot only these fronds (by name). Absent = every discovered frond. */
22
+ fronds?: string[];
23
+ /**
24
+ * Follow `remotes:` from config (default true). A host process (`serve`)
25
+ * passes false — it *is* the frond, it doesn't route back out.
26
+ */
27
+ topology?: boolean;
28
+ /**
29
+ * What this app takes on beyond its fronds — `observability()` is the first one.
30
+ * Appended after the framework's own members, or replacing one by naming it.
31
+ */
32
+ extensions?: CreateAppOptions['extensions'];
33
+ }
34
+
35
+ /**
36
+ * Boot a Fougere app from the `fougere.config.ts` at `root`, wired the
37
+ * conventional way (container + sqlite + http remotes).
38
+ */
39
+ export async function bootAppFromConfig(root: string, opts: BootAppOptions = {}): Promise<App> {
40
+ const config = await loadConfig(root);
41
+ const remotes = config.remotes ?? {};
42
+ const useRemotes = (opts.topology ?? true) && Object.keys(remotes).length > 0;
43
+
44
+ return boot({
45
+ root,
46
+ createContainer,
47
+ fronds: opts.fronds,
48
+ remotes: useRemotes ? remotes : undefined,
49
+ remoteTransport: useRemotes ? (url) => createHttpTransport(url) : undefined,
50
+ // One resolver, one place that knows a storage package.
51
+ db: (cfg) => resolveStorage(cfg.db as DbConfig, (cfg as { sources?: unknown }).sources as never),
52
+ extensions: opts.extensions,
53
+ });
54
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Storage resolution — `config.db` → a working data layer.
3
+ *
4
+ * THE single place that names a storage package. Every host (the conventional
5
+ * boot, the Nuxt fallback, the CLI's frond host) calls this instead of wiring an
6
+ * engine itself; swapping the implementation is a change here and nowhere else.
7
+ *
8
+ * Before this existed, each host re-resolved `db: 'sqlite'` inline — which is
9
+ * why an engine change looked like it touched seven files.
10
+ */
11
+ import type { App } from '@fougere/core';
12
+ import { Fronds, type FrondDescriptor } from '@fougere/core';
13
+ import { lowerFirst } from '@fougere/core/contract';
14
+ import { migrate, type DialectName, type Setup } from '@fougere/adapter-sql';
15
+ import { setupSqlite } from '@fougere/adapter-sql/sqlite';
16
+
17
+ /** The `db` field of fougere.config.ts, read structurally. */
18
+ export type DbConfig =
19
+ | false
20
+ | 'sqlite'
21
+ | { dialect?: string; path?: string }
22
+ | undefined;
23
+
24
+ /** A named source and the entities it holds — the `sources` field, read structurally. */
25
+ export type SourcesConfig = Record<string, { dialect?: string; path?: string; entities: string[] }> | undefined;
26
+
27
+ export interface ResolvedStorage {
28
+ /** Opaque handle handed to auth providers. */
29
+ db?: unknown;
30
+ ormFactory: ((entity: any, name: string) => any) | undefined;
31
+ /**
32
+ * The source an entity's rows live in — what decides whether a frame gets a real
33
+ * transaction or an unwind it replays itself.
34
+ *
35
+ * Absent when the caller built a `ResolvedStorage` by hand: there is then one factory
36
+ * and no routing, so every entity IS on one engine — but nothing here can reach into it
37
+ * for a transaction, and a frame falls back to compensating. Not knowing and promising
38
+ * atomicity are two different things.
39
+ */
40
+ sourceOf?: (entityName: string) => string;
41
+ /**
42
+ * The engine a source runs on — the dual of `sourceOf`, which names it without
43
+ * reaching it. A caller that must run DDL per engine needs the handle, not the name.
44
+ */
45
+ dbOf?: (source: string) => unknown;
46
+ /** Every source that has an engine, the default one first. */
47
+ sources?: () => string[];
48
+ /** Run `fn` inside one transaction of that source, with an ORM factory bound to it. */
49
+ transacted?: <R>(source: string, fn: (ormFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
50
+ /**
51
+ * Brings the schema up to date once the app is scanned — the storage's `up`, handed to
52
+ * `migrating()`. It was called `afterBoot`, a word that also meant the host's own
53
+ * post-boot and was read in four places under the two senses.
54
+ */
55
+ migrate?: (app: App) => Promise<void> | void;
56
+ /**
57
+ * Close every engine this opened — the dual of opening them, declared by whoever did.
58
+ *
59
+ * `boot()` calls the factory that lands here, so `boot()` is what owns closing it: a
60
+ * container disposes what IT built, and this connection was handed in. Without this
61
+ * the pool of a discarded app stayed open, which is what turning the ring makes
62
+ * ordinary rather than rare.
63
+ */
64
+ close?: () => Promise<void>;
65
+ /** Raw synchronous handle, when the engine exposes one. */
66
+ raw?: { exec(sql: string): void };
67
+ dialect?: DialectName;
68
+ }
69
+
70
+ /** Does this config ask for persistence at all? */
71
+ export function declaresStorage(dbConf: DbConfig): boolean {
72
+ if (dbConf === false || dbConf === undefined) return false;
73
+ return true;
74
+ }
75
+
76
+ /**
77
+ * Only sqlite is resolvable from a NAME — its driver is a dependency here. Any other
78
+ * engine needs a Kysely dialect INSTANCE, which only the host can build because only
79
+ * the host has its driver. So the name is refused, and the way in is named.
80
+ *
81
+ * `dialect` used to be declared and never read: `db: { dialect: 'postgres' }` started
82
+ * SQLite and said nothing — a config whose central word was ignored.
83
+ */
84
+ function refuseUnresolvable(declared: string | undefined, field: string): void {
85
+ if (declared === undefined || declared === 'sqlite') return;
86
+ throw new Error(
87
+ `${field} '${declared}' cannot be resolved from its name — only 'sqlite' can, ` +
88
+ `because it is the one driver this package depends on. For ${declared}, build the ` +
89
+ `Kysely dialect yourself and call setupKysely(dialect, '${declared}') ` +
90
+ `from @fougere/adapter-sql.`,
91
+ );
92
+ }
93
+
94
+ /**
95
+ * The app as ONE source sees it: the entities that live there, and the names of those
96
+ * that do not.
97
+ *
98
+ * The second half is what lets the DDL stop lying. A batch holding every entity could
99
+ * never tell a cross-source target from a typo, so it derived a table name and emitted
100
+ * a foreign key against a table that may not exist. Cut per source, `elsewhere` says
101
+ * which misses are legitimate — and a target in neither list is a mistake, out loud.
102
+ *
103
+ * Auth entities ride with the default source: a provider's tables are the app's own,
104
+ * and nothing yet lets one declare where it lives.
105
+ */
106
+ function partition(
107
+ app: App,
108
+ holds: (name: string) => boolean,
109
+ withAuth: boolean,
110
+ ): unknown {
111
+ const typed = app as unknown as {
112
+ fronds: { name: string; entities: { name: string }[] }[];
113
+ auth?: unknown;
114
+ };
115
+ const fronds = typed.fronds
116
+ .map((frond) => ({ ...frond, entities: frond.entities.filter((entry) => holds(entry.name)) }))
117
+ .filter((frond) => frond.entities.length > 0);
118
+ return {
119
+ fronds,
120
+ auth: withAuth ? typed.auth : undefined,
121
+ // Lifted, because this function reads its app structurally on purpose — a caller
122
+ // may hand it a shape that is app-LIKE, and the question is still the same one.
123
+ elsewhere: Fronds.scanned(typed.fronds as FrondDescriptor[]).entityNames().filter((name) => !holds(name)),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Resolve the data layer. `db: false` (or absent) means a frond with no
129
+ * persistence of its own — the caller decides what to fall back to.
130
+ *
131
+ * `sources` names the places that are NOT the default one. An entity it does not
132
+ * name stays in `db`, so an app with one database declares nothing and behaves
133
+ * exactly as before.
134
+ */
135
+ export function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage {
136
+ if (!declaresStorage(dbConf)) return { ormFactory: undefined };
137
+
138
+ refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
139
+ const named: Record<string, Placement> = {};
140
+ for (const [name, conf] of Object.entries(sources ?? {})) {
141
+ refuseUnresolvable(conf.dialect, `sources.${name}.dialect`);
142
+ named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
143
+ }
144
+ return storageFrom({
145
+ db: setupSqlite({ path: typeof dbConf === 'object' ? dbConf.path : undefined }),
146
+ sources: named,
147
+ });
148
+ }
149
+
150
+ /** One source: the engine that holds it, and the entities that live there. */
151
+ export interface Placement {
152
+ setup: Setup;
153
+ entities: string[];
154
+ }
155
+
156
+ export interface DeclaredStorage {
157
+ /** The default source — where an entity no placement names lands. */
158
+ db: Setup;
159
+ /** The other places. Absent means one source, the way it always was. */
160
+ sources?: Record<string, Placement>;
161
+ }
162
+
163
+ /**
164
+ * The same routing `resolveStorage` performs, over engines the CALLER built.
165
+ *
166
+ * `resolveStorage` reads a config file, and a config file cannot hold a live Kysely
167
+ * dialect — so it can only ever resolve sqlite, the one driver this package depends
168
+ * on. That was fine while `db:` was alone, because the escape hatch was to abandon
169
+ * the convention entirely and hand `configureFougere` your own factory. With several
170
+ * sources that stopped being an escape: a user wanting Postgres for ONE of them had
171
+ * to re-implement the routing and the per-source migration to keep the other.
172
+ *
173
+ * So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
174
+ * stays there; placing entities and migrating each source is engine-agnostic and
175
+ * lives here, where any `Setup` is welcome:
176
+ *
177
+ * ```ts
178
+ * configureFougere(storageFrom({
179
+ * db: setupSqlite({ path: '.data/app.db' }),
180
+ * sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
181
+ * }));
182
+ * ```
183
+ */
184
+ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
185
+ const { db: base, sources } = declared;
186
+
187
+ // Where each named entity lives. An entity claimed by two sources is refused naming
188
+ // both: the rows would be read from one and written to the other, by whichever
189
+ // registration ran last — the same silent duplicate `remotes:` refuses one level up.
190
+ const home = new Map<string, string>();
191
+ const engines = new Map<string, Setup>();
192
+ for (const [name, placement] of Object.entries(sources ?? {})) {
193
+ engines.set(name, placement.setup);
194
+ for (const entity of placement.entities) {
195
+ const key = lowerFirst(entity);
196
+ const claimed = home.get(key);
197
+ if (claimed) {
198
+ throw new Error(
199
+ `sources: '${entity}' is claimed by both '${claimed}' and '${name}' — an entity lives in one place.`,
200
+ );
201
+ }
202
+ home.set(key, name);
203
+ }
204
+ }
205
+
206
+ // The default source answers to the name the config gave it, so a refusal naming two
207
+ // sources names things the author can find in their own file.
208
+ const DEFAULT = 'db';
209
+ const sourceOf = (entityName: string) => home.get(lowerFirst(entityName)) ?? DEFAULT;
210
+ const engineOf = (source: string) => (source === DEFAULT ? base : engines.get(source));
211
+ const engineFor = (entityName: string) => {
212
+ const source = home.get(lowerFirst(entityName));
213
+ return (source && engines.get(source)) || base;
214
+ };
215
+
216
+ return {
217
+ db: base.db,
218
+ ormFactory: (entity: any, name: string) => engineFor(name).ormFactory(entity, name),
219
+ sourceOf,
220
+ dbOf: (source) => engineOf(source)?.db,
221
+ sources: () => [DEFAULT, ...engines.keys()],
222
+ transacted: (source, fn) => {
223
+ const engine = engineOf(source);
224
+ if (!engine) throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
225
+ return engine.transacted(fn);
226
+ },
227
+ raw: (base as { sqlite?: { exec(sql: string): void } }).sqlite,
228
+ dialect: base.dialect,
229
+ // Additive migration: creates missing tables AND adds columns an entity gained.
230
+ // One pass per source, each seeing only its own tables — which is what makes a
231
+ // cross-source `ref()` a miss rather than a constraint against a stranger.
232
+ migrate: async (app) => {
233
+ await migrate(partition(app, (name) => !home.has(lowerFirst(name)), true) as never, base.db);
234
+ for (const [name, engine] of engines) {
235
+ await migrate(partition(app, (e) => home.get(lowerFirst(e)) === name, false) as never, engine.db);
236
+ }
237
+ },
238
+ // Every engine, the default one last: a named source may hold what the default
239
+ // refers to, and closing in reverse of opening is the rule everywhere else.
240
+ close: async () => {
241
+ for (const engine of [...engines.values()].reverse()) await engine.db.destroy();
242
+ await base.db.destroy();
243
+ },
244
+ };
245
+ }