@fougere/defaults 0.2.0-alpha.2 → 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.
- package/README.md +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -11
- package/dist/index.js.map +1 -1
- package/dist/storage.d.ts +34 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +33 -14
- package/dist/storage.js.map +1 -1
- package/package.json +8 -7
- package/src/index.ts +54 -0
- package/src/storage.ts +251 -0
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://
|
|
15
|
+
Reference documentation: [the site](https://fougere.dev/) (en/fr).
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* package supplies them on top of `core.boot()`. Nuxt's fallback, the CLI's
|
|
9
9
|
* `serve`/`call`, and a standalone frond host are all projections of it.
|
|
10
10
|
*/
|
|
11
|
-
import { type App } from '@fougere/core';
|
|
11
|
+
import { type App, type CreateAppOptions } from '@fougere/core';
|
|
12
12
|
export { resolveStorage, declaresStorage, storageFrom } from './storage.js';
|
|
13
13
|
export type { DbConfig, SourcesConfig, DeclaredStorage, Placement, ResolvedStorage } from './storage.js';
|
|
14
14
|
export interface BootAppOptions {
|
|
@@ -19,6 +19,11 @@ export interface BootAppOptions {
|
|
|
19
19
|
* passes false — it *is* the frond, it doesn't route back out.
|
|
20
20
|
*/
|
|
21
21
|
topology?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* What this app takes on beyond its fronds — `observability()` is the first one.
|
|
24
|
+
* Appended after the framework's own members, or replacing one by naming it.
|
|
25
|
+
*/
|
|
26
|
+
extensions?: CreateAppOptions['extensions'];
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Boot a Fougere app from the `fougere.config.ts` at `root`, wired the
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAMhE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5E,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzG,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,CAe7F"}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
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 { boot, loadConfig } from '@fougere/core';
|
|
1
|
+
import { boot, loadConfig } from '@fougere/core/node';
|
|
12
2
|
import { createContainer } from '@fougere/container';
|
|
13
3
|
import { createHttpTransport } from '@fougere/transport-http';
|
|
14
4
|
import { resolveStorage } from './storage.js';
|
|
@@ -29,6 +19,7 @@ export async function bootAppFromConfig(root, opts = {}) {
|
|
|
29
19
|
remoteTransport: useRemotes ? (url) => createHttpTransport(url) : undefined,
|
|
30
20
|
// One resolver, one place that knows a storage package.
|
|
31
21
|
db: (cfg) => resolveStorage(cfg.db, cfg.sources),
|
|
22
|
+
extensions: opts.extensions,
|
|
32
23
|
});
|
|
33
24
|
}
|
|
34
25
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAiB,MAAM,cAAc,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAkB5E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAI,GAAmB,EAAE;IAC7E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IACrC,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAE9E,OAAO,IAAI,CAAC;QACV,IAAI;QACJ,eAAe;QACf,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACzC,eAAe,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;QAC3E,wDAAwD;QACxD,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,EAAc,EAAG,GAA6B,CAAC,OAAgB,CAAC;QAChG,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC,CAAC;AACL,CAAC"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -25,8 +25,40 @@ export interface ResolvedStorage {
|
|
|
25
25
|
/** Opaque handle handed to auth providers. */
|
|
26
26
|
db?: unknown;
|
|
27
27
|
ormFactory: ((entity: any, name: string) => any) | undefined;
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
30
|
+
* transaction or an unwind it replays itself.
|
|
31
|
+
*
|
|
32
|
+
* Absent when the caller built a `ResolvedStorage` by hand: there is then one factory
|
|
33
|
+
* and no routing, so every entity IS on one engine — but nothing here can reach into it
|
|
34
|
+
* for a transaction, and a frame falls back to compensating. Not knowing and promising
|
|
35
|
+
* atomicity are two different things.
|
|
36
|
+
*/
|
|
37
|
+
sourceOf?: (entityName: string) => string;
|
|
38
|
+
/**
|
|
39
|
+
* The engine a source runs on — the dual of `sourceOf`, which names it without
|
|
40
|
+
* reaching it. A caller that must run DDL per engine needs the handle, not the name.
|
|
41
|
+
*/
|
|
42
|
+
dbOf?: (source: string) => unknown;
|
|
43
|
+
/** Every source that has an engine, the default one first. */
|
|
44
|
+
sources?: () => string[];
|
|
45
|
+
/** Run `fn` inside one transaction of that source, with an ORM factory bound to it. */
|
|
46
|
+
transacted?: <R>(source: string, fn: (ormFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
47
|
+
/**
|
|
48
|
+
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
49
|
+
* `migrating()`. It was called `afterBoot`, a word that also meant the host's own
|
|
50
|
+
* post-boot and was read in four places under the two senses.
|
|
51
|
+
*/
|
|
52
|
+
migrate?: (app: App) => Promise<void> | void;
|
|
53
|
+
/**
|
|
54
|
+
* Close every engine this opened — the dual of opening them, declared by whoever did.
|
|
55
|
+
*
|
|
56
|
+
* `boot()` calls the factory that lands here, so `boot()` is what owns closing it: a
|
|
57
|
+
* container disposes what IT built, and this connection was handed in. Without this
|
|
58
|
+
* the pool of a discarded app stayed open, which is what turning the ring makes
|
|
59
|
+
* ordinary rather than rare.
|
|
60
|
+
*/
|
|
61
|
+
close?: () => Promise<void>;
|
|
30
62
|
/** Raw synchronous handle, when the engine exposes one. */
|
|
31
63
|
raw?: {
|
|
32
64
|
exec(sql: string): void;
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fronds } from '@fougere/core';
|
|
2
|
+
import { lowerFirst } from '@fougere/core/contract';
|
|
3
|
+
import { migrate } from '@fougere/adapter-sql';
|
|
4
|
+
import { setupSqlite } from '@fougere/adapter-sql/sqlite';
|
|
2
5
|
/** Does this config ask for persistence at all? */
|
|
3
6
|
export function declaresStorage(dbConf) {
|
|
4
7
|
if (dbConf === false || dbConf === undefined)
|
|
@@ -21,13 +24,6 @@ function refuseUnresolvable(declared, field) {
|
|
|
21
24
|
`Kysely dialect yourself and call setupKysely(dialect, '${declared}') ` +
|
|
22
25
|
`from @fougere/adapter-sql.`);
|
|
23
26
|
}
|
|
24
|
-
/** The name an entity is registered under — 'Book' and 'book' name the same rows. */
|
|
25
|
-
const keyOf = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
26
|
-
/** Every entity the app hosts, wherever its rows are — what a partition is cut from. */
|
|
27
|
-
function entityNames(app) {
|
|
28
|
-
return app
|
|
29
|
-
.fronds.flatMap((frond) => frond.entities.map((entry) => entry.name));
|
|
30
|
-
}
|
|
31
27
|
/**
|
|
32
28
|
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
33
29
|
* that do not.
|
|
@@ -48,7 +44,9 @@ function partition(app, holds, withAuth, materialize) {
|
|
|
48
44
|
return {
|
|
49
45
|
fronds,
|
|
50
46
|
auth: withAuth ? typed.auth : undefined,
|
|
51
|
-
|
|
47
|
+
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
48
|
+
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
49
|
+
elsewhere: Fronds.scanned(typed.fronds).entityNames().filter((name) => !holds(name)),
|
|
52
50
|
// A derivation makes no table unless a source names it — see AppLike.materialize.
|
|
53
51
|
materialize,
|
|
54
52
|
};
|
|
@@ -108,7 +106,7 @@ export function storageFrom(declared) {
|
|
|
108
106
|
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
109
107
|
engines.set(name, placement.setup);
|
|
110
108
|
for (const entity of placement.entities) {
|
|
111
|
-
const key =
|
|
109
|
+
const key = lowerFirst(entity);
|
|
112
110
|
const claimed = home.get(key);
|
|
113
111
|
if (claimed) {
|
|
114
112
|
throw new Error(`sources: '${entity}' is claimed by both '${claimed}' and '${name}' — an entity lives in one place.`);
|
|
@@ -117,24 +115,45 @@ export function storageFrom(declared) {
|
|
|
117
115
|
named.push(entity);
|
|
118
116
|
}
|
|
119
117
|
}
|
|
118
|
+
// The default source answers to the name the config gave it, so a refusal naming two
|
|
119
|
+
// sources names things the author can find in their own file.
|
|
120
|
+
const DEFAULT = 'db';
|
|
121
|
+
const sourceOf = (entityName) => home.get(lowerFirst(entityName)) ?? DEFAULT;
|
|
122
|
+
const engineOf = (source) => (source === DEFAULT ? base : engines.get(source));
|
|
120
123
|
const engineFor = (entityName) => {
|
|
121
|
-
const source = home.get(
|
|
124
|
+
const source = home.get(lowerFirst(entityName));
|
|
122
125
|
return (source && engines.get(source)) || base;
|
|
123
126
|
};
|
|
124
127
|
return {
|
|
125
128
|
db: base.db,
|
|
126
129
|
ormFactory: (entity, name) => engineFor(name).ormFactory(entity, name),
|
|
130
|
+
sourceOf,
|
|
131
|
+
dbOf: (source) => engineOf(source)?.db,
|
|
132
|
+
sources: () => [DEFAULT, ...engines.keys()],
|
|
133
|
+
transacted: (source, fn) => {
|
|
134
|
+
const engine = engineOf(source);
|
|
135
|
+
if (!engine)
|
|
136
|
+
throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
137
|
+
return engine.transacted(fn);
|
|
138
|
+
},
|
|
127
139
|
raw: base.sqlite,
|
|
128
140
|
dialect: base.dialect,
|
|
129
141
|
// Additive migration: creates missing tables AND adds columns an entity gained.
|
|
130
142
|
// One pass per source, each seeing only its own tables — which is what makes a
|
|
131
143
|
// cross-source `ref()` a miss rather than a constraint against a stranger.
|
|
132
|
-
|
|
133
|
-
await migrate(partition(app, (name) => !home.has(
|
|
144
|
+
migrate: async (app) => {
|
|
145
|
+
await migrate(partition(app, (name) => !home.has(lowerFirst(name)), true, named), base.db);
|
|
134
146
|
for (const [name, engine] of engines) {
|
|
135
|
-
await migrate(partition(app, (e) => home.get(
|
|
147
|
+
await migrate(partition(app, (e) => home.get(lowerFirst(e)) === name, false, named), engine.db);
|
|
136
148
|
}
|
|
137
149
|
},
|
|
150
|
+
// Every engine, the default one last: a named source may hold what the default
|
|
151
|
+
// refers to, and closing in reverse of opening is the rule everywhere else.
|
|
152
|
+
close: async () => {
|
|
153
|
+
for (const engine of [...engines.values()].reverse())
|
|
154
|
+
await engine.db.destroy();
|
|
155
|
+
await base.db.destroy();
|
|
156
|
+
},
|
|
138
157
|
};
|
|
139
158
|
}
|
|
140
159
|
//# sourceMappingURL=storage.js.map
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,MAAM,EAAwB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAgC,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAuD1D,mDAAmD;AACnD,MAAM,UAAU,eAAe,CAAC,MAAgB;IAC9C,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC3D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,QAA4B,EAAE,KAAa;IACrE,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO;IAC5D,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,KAAK,QAAQ,0DAA0D;QAC/E,6DAA6D,QAAQ,cAAc;QACnF,0DAA0D,QAAQ,KAAK;QACvE,4BAA4B,CAC7B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,SAAS,CAChB,GAAQ,EACR,KAAgC,EAChC,QAAiB,EACjB,WAAqB;IAErB,MAAM,KAAK,GAAG,GAGb,CAAC;IACF,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM;SACxB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;SAC7F,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChD,OAAO;QACL,MAAM;QACN,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACvC,iFAAiF;QACjF,gFAAgF;QAChF,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAA2B,CAAC,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzG,kFAAkF;QAClF,WAAW;KACZ,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAgB,EAAE,OAAuB;IACtE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;IAE/D,kBAAkB,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,SAAS,CAAC,EAAE,YAAY,CAAC,CAAC;IACtG,MAAM,KAAK,GAA8B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;QACzD,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,IAAI,UAAU,CAAC,CAAC;QAC5D,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IACrF,CAAC;IACD,OAAO,WAAW,CAAC;QACjB,EAAE,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;QAC/E,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;AACL,CAAC;AAeD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW,CAAC,QAAyB;IACnD,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC;IAEvC,oFAAoF;IACpF,+EAA+E;IAC/E,qFAAqF;IACrF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IACzC,uFAAuF;IACvF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;QACnC,KAAK,MAAM,MAAM,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,aAAa,MAAM,yBAAyB,OAAO,UAAU,IAAI,mCAAmC,CACrG,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,8DAA8D;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC;IACrB,MAAM,QAAQ,GAAG,CAAC,UAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC;IACrF,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACvF,MAAM,SAAS,GAAG,CAAC,UAAkB,EAAE,EAAE;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;QAChD,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC;IACjD,CAAC,CAAC;IAEF,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,UAAU,EAAE,CAAC,MAAW,EAAE,IAAY,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;QACnF,QAAQ;QACR,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE;QACtC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC3C,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE;YACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;YAChC,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,4BAA4B,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/H,OAAO,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QACD,GAAG,EAAG,IAAiD,CAAC,MAAM;QAC9D,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,gFAAgF;QAChF,+EAA+E;QAC/E,2EAA2E;QAC3E,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAU,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YACpG,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;gBACrC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,KAAK,CAAU,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;YAC3G,CAAC;QACH,CAAC;QACD,+EAA+E;QAC/E,4EAA4E;QAC5E,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE;gBAAE,MAAM,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;YAChF,MAAM,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/defaults",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.
|
|
32
|
-
"@fougere/transport-http": "0.
|
|
33
|
-
"@fougere/
|
|
34
|
-
"@fougere/
|
|
32
|
+
"@fougere/core": "0.4.0-alpha.0",
|
|
33
|
+
"@fougere/transport-http": "0.4.0-alpha.0",
|
|
34
|
+
"@fougere/adapter-sql": "0.4.0-alpha.0",
|
|
35
|
+
"@fougere/container": "0.4.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.
|
|
44
|
+
"@fougere/schema": "0.4.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,251 @@
|
|
|
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
|
+
materialize: string[],
|
|
111
|
+
): unknown {
|
|
112
|
+
const typed = app as unknown as {
|
|
113
|
+
fronds: { name: string; entities: { name: string }[] }[];
|
|
114
|
+
auth?: unknown;
|
|
115
|
+
};
|
|
116
|
+
const fronds = typed.fronds
|
|
117
|
+
.map((frond) => ({ ...frond, entities: frond.entities.filter((entry) => holds(entry.name)) }))
|
|
118
|
+
.filter((frond) => frond.entities.length > 0);
|
|
119
|
+
return {
|
|
120
|
+
fronds,
|
|
121
|
+
auth: withAuth ? typed.auth : undefined,
|
|
122
|
+
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
123
|
+
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
124
|
+
elsewhere: Fronds.scanned(typed.fronds as FrondDescriptor[]).entityNames().filter((name) => !holds(name)),
|
|
125
|
+
// A derivation makes no table unless a source names it — see AppLike.materialize.
|
|
126
|
+
materialize,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the data layer. `db: false` (or absent) means a frond with no
|
|
132
|
+
* persistence of its own — the caller decides what to fall back to.
|
|
133
|
+
*
|
|
134
|
+
* `sources` names the places that are NOT the default one. An entity it does not
|
|
135
|
+
* name stays in `db`, so an app with one database declares nothing and behaves
|
|
136
|
+
* exactly as before.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage {
|
|
139
|
+
if (!declaresStorage(dbConf)) return { ormFactory: undefined };
|
|
140
|
+
|
|
141
|
+
refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
|
|
142
|
+
const named: Record<string, Placement> = {};
|
|
143
|
+
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
144
|
+
refuseUnresolvable(conf.dialect, `sources.${name}.dialect`);
|
|
145
|
+
named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
|
|
146
|
+
}
|
|
147
|
+
return storageFrom({
|
|
148
|
+
db: setupSqlite({ path: typeof dbConf === 'object' ? dbConf.path : undefined }),
|
|
149
|
+
sources: named,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** One source: the engine that holds it, and the entities that live there. */
|
|
154
|
+
export interface Placement {
|
|
155
|
+
setup: Setup;
|
|
156
|
+
entities: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface DeclaredStorage {
|
|
160
|
+
/** The default source — where an entity no placement names lands. */
|
|
161
|
+
db: Setup;
|
|
162
|
+
/** The other places. Absent means one source, the way it always was. */
|
|
163
|
+
sources?: Record<string, Placement>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The same routing `resolveStorage` performs, over engines the CALLER built.
|
|
168
|
+
*
|
|
169
|
+
* `resolveStorage` reads a config file, and a config file cannot hold a live Kysely
|
|
170
|
+
* dialect — so it can only ever resolve sqlite, the one driver this package depends
|
|
171
|
+
* on. That was fine while `db:` was alone, because the escape hatch was to abandon
|
|
172
|
+
* the convention entirely and hand `configureFougere` your own factory. With several
|
|
173
|
+
* sources that stopped being an escape: a user wanting Postgres for ONE of them had
|
|
174
|
+
* to re-implement the routing and the per-source migration to keep the other.
|
|
175
|
+
*
|
|
176
|
+
* So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
|
|
177
|
+
* stays there; placing entities and migrating each source is engine-agnostic and
|
|
178
|
+
* lives here, where any `Setup` is welcome:
|
|
179
|
+
*
|
|
180
|
+
* ```ts
|
|
181
|
+
* configureFougere(storageFrom({
|
|
182
|
+
* db: setupSqlite({ path: '.data/app.db' }),
|
|
183
|
+
* sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
|
|
184
|
+
* }));
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
188
|
+
const { db: base, sources } = declared;
|
|
189
|
+
|
|
190
|
+
// Where each named entity lives. An entity claimed by two sources is refused naming
|
|
191
|
+
// both: the rows would be read from one and written to the other, by whichever
|
|
192
|
+
// registration ran last — the same silent duplicate `remotes:` refuses one level up.
|
|
193
|
+
const home = new Map<string, string>();
|
|
194
|
+
const engines = new Map<string, Setup>();
|
|
195
|
+
/** Everything a source names — the opt-in that turns a derivation into stored rows. */
|
|
196
|
+
const named: string[] = [];
|
|
197
|
+
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
198
|
+
engines.set(name, placement.setup);
|
|
199
|
+
for (const entity of placement.entities) {
|
|
200
|
+
const key = lowerFirst(entity);
|
|
201
|
+
const claimed = home.get(key);
|
|
202
|
+
if (claimed) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`sources: '${entity}' is claimed by both '${claimed}' and '${name}' — an entity lives in one place.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
home.set(key, name);
|
|
208
|
+
named.push(entity);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The default source answers to the name the config gave it, so a refusal naming two
|
|
213
|
+
// sources names things the author can find in their own file.
|
|
214
|
+
const DEFAULT = 'db';
|
|
215
|
+
const sourceOf = (entityName: string) => home.get(lowerFirst(entityName)) ?? DEFAULT;
|
|
216
|
+
const engineOf = (source: string) => (source === DEFAULT ? base : engines.get(source));
|
|
217
|
+
const engineFor = (entityName: string) => {
|
|
218
|
+
const source = home.get(lowerFirst(entityName));
|
|
219
|
+
return (source && engines.get(source)) || base;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
db: base.db,
|
|
224
|
+
ormFactory: (entity: any, name: string) => engineFor(name).ormFactory(entity, name),
|
|
225
|
+
sourceOf,
|
|
226
|
+
dbOf: (source) => engineOf(source)?.db,
|
|
227
|
+
sources: () => [DEFAULT, ...engines.keys()],
|
|
228
|
+
transacted: (source, fn) => {
|
|
229
|
+
const engine = engineOf(source);
|
|
230
|
+
if (!engine) throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
231
|
+
return engine.transacted(fn);
|
|
232
|
+
},
|
|
233
|
+
raw: (base as { sqlite?: { exec(sql: string): void } }).sqlite,
|
|
234
|
+
dialect: base.dialect,
|
|
235
|
+
// Additive migration: creates missing tables AND adds columns an entity gained.
|
|
236
|
+
// One pass per source, each seeing only its own tables — which is what makes a
|
|
237
|
+
// cross-source `ref()` a miss rather than a constraint against a stranger.
|
|
238
|
+
migrate: async (app) => {
|
|
239
|
+
await migrate(partition(app, (name) => !home.has(lowerFirst(name)), true, named) as never, base.db);
|
|
240
|
+
for (const [name, engine] of engines) {
|
|
241
|
+
await migrate(partition(app, (e) => home.get(lowerFirst(e)) === name, false, named) as never, engine.db);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
// Every engine, the default one last: a named source may hold what the default
|
|
245
|
+
// refers to, and closing in reverse of opening is the rule everywhere else.
|
|
246
|
+
close: async () => {
|
|
247
|
+
for (const engine of [...engines.values()].reverse()) await engine.db.destroy();
|
|
248
|
+
await base.db.destroy();
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|