@fougere/defaults 0.5.0-alpha.1 → 0.7.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storage.d.ts +19 -68
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +38 -80
- package/dist/storage.js.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +1 -10
- package/src/storage.ts +59 -122
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +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
|
-
*/
|
|
1
|
+
/** @fougere/defaults — the conventional boot, declared once. */
|
|
11
2
|
import { type App, type CreateAppOptions } from '@fougere/core';
|
|
12
3
|
export { resolveStorage, declaresStorage, storageFrom } from './storage.js';
|
|
13
4
|
export type { DbConfig, SourcesConfig, DeclaredStorage, Placement, ResolvedStorage } from './storage.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,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.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":"AAEA,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
|
@@ -1,22 +1,16 @@
|
|
|
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
|
-
*/
|
|
1
|
+
/** Storage resolution — `config.db` → a working data layer. */
|
|
11
2
|
import type { App } from '@fougere/core';
|
|
12
|
-
import { type
|
|
3
|
+
import { type Source } from '@fougere/core';
|
|
4
|
+
import '@fougere/adapter-sql/sqlite';
|
|
13
5
|
/** The `db` field of fougere.config.ts, read structurally. */
|
|
14
6
|
export type DbConfig = false | 'sqlite' | {
|
|
7
|
+
source?: string;
|
|
15
8
|
dialect?: string;
|
|
16
9
|
path?: string;
|
|
17
10
|
} | undefined;
|
|
18
11
|
/** A named source and the entities it holds — the `sources` field, read structurally. */
|
|
19
12
|
export type SourcesConfig = Record<string, {
|
|
13
|
+
source?: string;
|
|
20
14
|
dialect?: string;
|
|
21
15
|
path?: string;
|
|
22
16
|
entities: string[];
|
|
@@ -24,15 +18,10 @@ export type SourcesConfig = Record<string, {
|
|
|
24
18
|
export interface ResolvedStorage {
|
|
25
19
|
/** Opaque handle handed to auth providers. */
|
|
26
20
|
db?: unknown;
|
|
27
|
-
|
|
21
|
+
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
28
22
|
/**
|
|
29
|
-
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
30
|
-
*
|
|
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.
|
|
23
|
+
* The source an entity's rows live in — what decides whether a frame gets a real transaction or
|
|
24
|
+
* an unwind it replays itself.
|
|
36
25
|
*/
|
|
37
26
|
sourceOf?: (entityName: string) => string;
|
|
38
27
|
/**
|
|
@@ -42,71 +31,33 @@ export interface ResolvedStorage {
|
|
|
42
31
|
dbOf?: (source: string) => unknown;
|
|
43
32
|
/** Every source that has an engine, the default one first. */
|
|
44
33
|
sources?: () => string[];
|
|
45
|
-
/**
|
|
46
|
-
|
|
34
|
+
/** Whether that source hands out a transaction — the dual of running one in it. */
|
|
35
|
+
transacts?: (source: string) => boolean;
|
|
36
|
+
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
37
|
+
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
47
38
|
/**
|
|
48
39
|
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
49
|
-
* `migrating()`.
|
|
50
|
-
* post-boot and was read in four places under the two senses.
|
|
40
|
+
* `migrating()`.
|
|
51
41
|
*/
|
|
52
42
|
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
|
-
*/
|
|
43
|
+
/** Close every engine this opened — the dual of opening them, declared by whoever did. */
|
|
61
44
|
close?: () => Promise<void>;
|
|
62
|
-
/** Raw synchronous handle, when the engine exposes one. */
|
|
63
|
-
raw?: {
|
|
64
|
-
exec(sql: string): void;
|
|
65
|
-
};
|
|
66
|
-
dialect?: DialectName;
|
|
67
45
|
}
|
|
68
46
|
/** Does this config ask for persistence at all? */
|
|
69
47
|
export declare function declaresStorage(dbConf: DbConfig): boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Resolve the data layer. `db: false` (or absent) means a frond with no
|
|
72
|
-
* persistence of its own — the caller decides what to fall back to.
|
|
73
|
-
*
|
|
74
|
-
* `sources` names the places that are NOT the default one. An entity it does not
|
|
75
|
-
* name stays in `db`, so an app with one database declares nothing and behaves
|
|
76
|
-
* exactly as before.
|
|
77
|
-
*/
|
|
48
|
+
/** Resolve the data layer. */
|
|
78
49
|
export declare function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage;
|
|
79
|
-
/** One source:
|
|
50
|
+
/** One source: what realizes it, and the entities that live there. */
|
|
80
51
|
export interface Placement {
|
|
81
|
-
|
|
52
|
+
source: Source;
|
|
82
53
|
entities: string[];
|
|
83
54
|
}
|
|
84
55
|
export interface DeclaredStorage {
|
|
85
56
|
/** The default source — where an entity no placement names lands. */
|
|
86
|
-
db:
|
|
57
|
+
db: Source;
|
|
87
58
|
/** The other places. Absent means one source, the way it always was. */
|
|
88
59
|
sources?: Record<string, Placement>;
|
|
89
60
|
}
|
|
90
|
-
/**
|
|
91
|
-
* The same routing `resolveStorage` performs, over engines the CALLER built.
|
|
92
|
-
*
|
|
93
|
-
* `resolveStorage` reads a config file, and a config file cannot hold a live Kysely
|
|
94
|
-
* dialect — so it can only ever resolve sqlite, the one driver this package depends
|
|
95
|
-
* on. That was fine while `db:` was alone, because the escape hatch was to abandon
|
|
96
|
-
* the convention entirely and hand `configureFougere` your own factory. With several
|
|
97
|
-
* sources that stopped being an escape: a user wanting Postgres for ONE of them had
|
|
98
|
-
* to re-implement the routing and the per-source migration to keep the other.
|
|
99
|
-
*
|
|
100
|
-
* So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
|
|
101
|
-
* stays there; placing entities and migrating each source is engine-agnostic and
|
|
102
|
-
* lives here, where any `Setup` is welcome:
|
|
103
|
-
*
|
|
104
|
-
* ```ts
|
|
105
|
-
* configureFougere(storageFrom({
|
|
106
|
-
* db: setupSqlite({ path: '.data/app.db' }),
|
|
107
|
-
* sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
|
|
108
|
-
* }));
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
61
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
111
62
|
export declare function storageFrom(declared: DeclaredStorage): ResolvedStorage;
|
|
112
63
|
//# sourceMappingURL=storage.d.ts.map
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAW,KAAK,MAAM,EAAmB,MAAM,eAAe,CAAC;AAEtE,OAAO,6BAA6B,CAAC;AAErC,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAChB,KAAK,GACL,QAAQ,GACR;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,SAAS,CAAC;AAEd,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC;AAEjI,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IACjE;;;OAGG;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,mFAAmF;IACnF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,0FAA0F;IAC1F,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACvH;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7C,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAE7B;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAGzD;AAyBD,8BAA8B;AAC9B,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAWzF;AAYD,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACrC;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CAkEtE"}
|
package/dist/storage.js
CHANGED
|
@@ -1,42 +1,16 @@
|
|
|
1
1
|
import { Fronds } from '@fougere/core';
|
|
2
2
|
import { lowerFirst } from '@fougere/core/contract';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { Sources } from '@fougere/core';
|
|
4
|
+
// Imported for its side effect: it is what makes `source: 'sql'` an answered name.
|
|
5
|
+
import '@fougere/adapter-sql/sqlite';
|
|
5
6
|
/** Does this config ask for persistence at all? */
|
|
6
7
|
export function declaresStorage(dbConf) {
|
|
7
8
|
if (dbConf === false || dbConf === undefined)
|
|
8
9
|
return false;
|
|
9
10
|
return true;
|
|
10
11
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
* engine needs a Kysely dialect INSTANCE, which only the host can build because only
|
|
14
|
-
* the host has its driver. So the name is refused, and the way in is named.
|
|
15
|
-
*
|
|
16
|
-
* `dialect` used to be declared and never read: `db: { dialect: 'postgres' }` started
|
|
17
|
-
* SQLite and said nothing — a config whose central word was ignored.
|
|
18
|
-
*/
|
|
19
|
-
function refuseUnresolvable(declared, field) {
|
|
20
|
-
if (declared === undefined || declared === 'sqlite')
|
|
21
|
-
return;
|
|
22
|
-
throw new Error(`${field} '${declared}' cannot be resolved from its name — only 'sqlite' can, ` +
|
|
23
|
-
`because it is the one driver this package depends on. For ${declared}, build the ` +
|
|
24
|
-
`Kysely dialect yourself and call setupKysely(dialect, '${declared}') ` +
|
|
25
|
-
`from @fougere/adapter-sql.`);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
29
|
-
* that do not.
|
|
30
|
-
*
|
|
31
|
-
* The second half is what lets the DDL stop lying. A batch holding every entity could
|
|
32
|
-
* never tell a cross-source target from a typo, so it derived a table name and emitted
|
|
33
|
-
* a foreign key against a table that may not exist. Cut per source, `elsewhere` says
|
|
34
|
-
* which misses are legitimate — and a target in neither list is a mistake, out loud.
|
|
35
|
-
*
|
|
36
|
-
* Auth entities ride with the default source: a provider's tables are the app's own,
|
|
37
|
-
* and nothing yet lets one declare where it lives.
|
|
38
|
-
*/
|
|
39
|
-
function partition(app, holds, withAuth) {
|
|
12
|
+
/** The app as ONE source sees it. */
|
|
13
|
+
function viewOf(app, holds, withAuth) {
|
|
40
14
|
const typed = app;
|
|
41
15
|
const fronds = typed.fronds
|
|
42
16
|
.map((frond) => ({ ...frond, entities: frond.entities.filter((entry) => holds(entry.name)) }))
|
|
@@ -46,52 +20,30 @@ function partition(app, holds, withAuth) {
|
|
|
46
20
|
auth: withAuth ? typed.auth : undefined,
|
|
47
21
|
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
48
22
|
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
49
|
-
elsewhere: Fronds.
|
|
23
|
+
elsewhere: Fronds.hosting(typed.fronds).entityNames().filter((name) => !holds(name)),
|
|
50
24
|
};
|
|
51
25
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Resolve the data layer. `db: false` (or absent) means a frond with no
|
|
54
|
-
* persistence of its own — the caller decides what to fall back to.
|
|
55
|
-
*
|
|
56
|
-
* `sources` names the places that are NOT the default one. An entity it does not
|
|
57
|
-
* name stays in `db`, so an app with one database declares nothing and behaves
|
|
58
|
-
* exactly as before.
|
|
59
|
-
*/
|
|
26
|
+
/** Resolve the data layer. */
|
|
60
27
|
export function resolveStorage(dbConf, sources) {
|
|
61
28
|
if (!declaresStorage(dbConf))
|
|
62
|
-
return {
|
|
63
|
-
refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
|
|
29
|
+
return { storageFactory: undefined };
|
|
64
30
|
const named = {};
|
|
65
31
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
66
|
-
|
|
67
|
-
named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
|
|
32
|
+
named[name] = { source: built(conf, `sources.${name}`), entities: conf.entities };
|
|
68
33
|
}
|
|
69
34
|
return storageFrom({
|
|
70
|
-
db:
|
|
35
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
71
36
|
sources: named,
|
|
72
37
|
});
|
|
73
38
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* to re-implement the routing and the per-source migration to keep the other.
|
|
83
|
-
*
|
|
84
|
-
* So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
|
|
85
|
-
* stays there; placing entities and migrating each source is engine-agnostic and
|
|
86
|
-
* lives here, where any `Setup` is welcome:
|
|
87
|
-
*
|
|
88
|
-
* ```ts
|
|
89
|
-
* configureFougere(storageFrom({
|
|
90
|
-
* db: setupSqlite({ path: '.data/app.db' }),
|
|
91
|
-
* sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
|
|
92
|
-
* }));
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
39
|
+
/** One config entry, resolved to the adapter it names. */
|
|
40
|
+
function built(conf, field) {
|
|
41
|
+
const name = conf.source ?? DEFAULT_ADAPTER;
|
|
42
|
+
return Sources.open(name, conf, `${field}.source`);
|
|
43
|
+
}
|
|
44
|
+
/** What a config naming no adapter means. */
|
|
45
|
+
const DEFAULT_ADAPTER = 'sql';
|
|
46
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
95
47
|
export function storageFrom(declared) {
|
|
96
48
|
const { db: base, sources } = declared;
|
|
97
49
|
// Where each named entity lives. An entity claimed by two sources is refused naming
|
|
@@ -100,7 +52,7 @@ export function storageFrom(declared) {
|
|
|
100
52
|
const home = new Map();
|
|
101
53
|
const engines = new Map();
|
|
102
54
|
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
103
|
-
engines.set(name, placement.
|
|
55
|
+
engines.set(name, placement.source);
|
|
104
56
|
for (const entity of placement.entities) {
|
|
105
57
|
const key = lowerFirst(entity);
|
|
106
58
|
const claimed = home.get(key);
|
|
@@ -120,34 +72,40 @@ export function storageFrom(declared) {
|
|
|
120
72
|
return (source && engines.get(source)) || base;
|
|
121
73
|
};
|
|
122
74
|
return {
|
|
75
|
+
// Opaque, and narrowed by whoever needs it — auth wants a handle, the CLI wants Kysely.
|
|
123
76
|
db: base.db,
|
|
124
|
-
|
|
77
|
+
storageFactory: (entity, name) => engineFor(name).storageFactory(entity, name),
|
|
125
78
|
sourceOf,
|
|
126
79
|
dbOf: (source) => engineOf(source)?.db,
|
|
127
80
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
128
|
-
|
|
81
|
+
// Asked of the source that will run the work, never of the default one: a frame whose
|
|
82
|
+
// members all live in a transactional source gets its transaction even when `db:` has
|
|
83
|
+
// none. A source that hands out none leaves the answer `false`, and a frame reads it:
|
|
84
|
+
// `boot/together.ts` compensates instead, and says which of the two it built.
|
|
85
|
+
transacts: (source) => engineOf(source)?.transacted !== undefined,
|
|
86
|
+
transacted: async (source, fn) => {
|
|
129
87
|
const engine = engineOf(source);
|
|
130
88
|
if (!engine)
|
|
131
89
|
throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
90
|
+
if (!engine.transacted)
|
|
91
|
+
throw new Error(`Source '${source}' hands out no transaction.`);
|
|
132
92
|
return engine.transacted(fn);
|
|
133
93
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
// One pass per source, each seeing only its own tables — which is what makes a
|
|
138
|
-
// cross-source `ref()` a miss rather than a constraint against a stranger.
|
|
94
|
+
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
95
|
+
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
96
|
+
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|
|
139
97
|
migrate: async (app) => {
|
|
140
|
-
await migrate(
|
|
98
|
+
await base.migrate?.(viewOf(app, (name) => !home.has(lowerFirst(name)), true));
|
|
141
99
|
for (const [name, engine] of engines) {
|
|
142
|
-
await migrate(
|
|
100
|
+
await engine.migrate?.(viewOf(app, (e) => home.get(lowerFirst(e)) === name, false));
|
|
143
101
|
}
|
|
144
102
|
},
|
|
145
|
-
// Every
|
|
146
|
-
//
|
|
103
|
+
// Every source, the default one last: a named source may hold what the default refers
|
|
104
|
+
// to, and closing in reverse of opening is the rule everywhere else.
|
|
147
105
|
close: async () => {
|
|
148
106
|
for (const engine of [...engines.values()].reverse())
|
|
149
|
-
await engine.
|
|
150
|
-
await base.
|
|
107
|
+
await engine.close?.();
|
|
108
|
+
await base.close?.();
|
|
151
109
|
},
|
|
152
110
|
};
|
|
153
111
|
}
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAwB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAgC,MAAM,eAAe,CAAC;AACtE,mFAAmF;AACnF,OAAO,6BAA6B,CAAC;AA0CrC,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;AAGD,qCAAqC;AACrC,SAAS,MAAM,CACb,GAAQ,EACR,KAAgC,EAChC,QAAiB;IAEjB,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;KAC1G,CAAC;AACJ,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,cAAc,CAAC,MAAgB,EAAE,OAAuB;IACtE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,CAAC;IAEnE,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,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IACpF,CAAC;IACD,OAAO,WAAW,CAAC;QACjB,EAAE,EAAE,KAAK,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC;QACzD,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;AACL,CAAC;AAED,0DAA0D;AAC1D,SAAS,KAAK,CAAC,IAA6B,EAAE,KAAa;IACzD,MAAM,IAAI,GAAI,IAAI,CAAC,MAA6B,IAAI,eAAe,CAAC;IAEpE,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAa,EAAE,GAAG,KAAK,SAAS,CAAC,CAAC;AAC9D,CAAC;AAED,6CAA6C;AAC7C,MAAM,eAAe,GAAG,KAAK,CAAC;AAe9B,iFAAiF;AACjF,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,EAAkB,CAAC;IAC1C,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,MAAM,CAAC,CAAC;QACpC,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;QACtB,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,wFAAwF;QACxF,EAAE,EAAG,IAAyB,CAAC,EAAE;QACjC,cAAc,EAAE,CAAC,MAAW,EAAE,IAAY,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC;QAC3F,QAAQ;QACR,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAE,QAAQ,CAAC,MAAM,CAAkC,EAAE,EAAE;QACxE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC3C,sFAAsF;QACtF,sFAAsF;QACtF,sFAAsF;QACtF,8EAA8E;QAC9E,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,KAAK,SAAS;QACjE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;YAC/B,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,IAAI,CAAC,MAAM,CAAC,UAAU;gBAAE,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,6BAA6B,CAAC,CAAC;YACxF,OAAO,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QACD,uFAAuF;QACvF,uFAAuF;QACvF,sFAAsF;QACtF,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;YAC/E,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;gBACrC,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;QACD,sFAAsF;QACtF,qEAAqE;QACrE,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE;gBAAE,MAAM,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;YAC7E,MAAM,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;QACvB,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.7.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",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@fougere/core": "0.
|
|
33
|
-
"@fougere/adapter-sql": "0.
|
|
34
|
-
"@fougere/container": "0.
|
|
35
|
-
"@fougere/transport-http": "0.
|
|
32
|
+
"@fougere/core": "0.7.0-alpha.0",
|
|
33
|
+
"@fougere/adapter-sql": "0.7.0-alpha.0",
|
|
34
|
+
"@fougere/container": "0.7.0-alpha.0",
|
|
35
|
+
"@fougere/transport-http": "0.7.0-alpha.0"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"kysely": "^0.28.17",
|
|
42
42
|
"better-sqlite3": "^13.0.3",
|
|
43
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
44
|
-
"@fougere/schema": "0.
|
|
44
|
+
"@fougere/schema": "0.7.0-alpha.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "rm -rf dist && tsc",
|
package/src/index.ts
CHANGED
|
@@ -1,13 +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
|
-
*/
|
|
1
|
+
/** @fougere/defaults — the conventional boot, declared once. */
|
|
11
2
|
import { type App, type CreateAppOptions } from '@fougere/core';
|
|
12
3
|
import { boot, loadConfig } from '@fougere/core/node';
|
|
13
4
|
import { createContainer } from '@fougere/container';
|
package/src/storage.ts
CHANGED
|
@@ -1,41 +1,28 @@
|
|
|
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
|
-
*/
|
|
1
|
+
/** Storage resolution — `config.db` → a working data layer. */
|
|
11
2
|
import type { App } from '@fougere/core';
|
|
12
3
|
import { Fronds, type FrondDescriptor } from '@fougere/core';
|
|
13
4
|
import { lowerFirst } from '@fougere/core/contract';
|
|
14
|
-
import {
|
|
15
|
-
|
|
5
|
+
import { Sources, type Source, type SourceView } from '@fougere/core';
|
|
6
|
+
// Imported for its side effect: it is what makes `source: 'sql'` an answered name.
|
|
7
|
+
import '@fougere/adapter-sql/sqlite';
|
|
16
8
|
|
|
17
9
|
/** The `db` field of fougere.config.ts, read structurally. */
|
|
18
10
|
export type DbConfig =
|
|
19
11
|
| false
|
|
20
12
|
| 'sqlite'
|
|
21
|
-
| { dialect?: string; path?: string }
|
|
13
|
+
| { source?: string; dialect?: string; path?: string }
|
|
22
14
|
| undefined;
|
|
23
15
|
|
|
24
16
|
/** 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;
|
|
17
|
+
export type SourcesConfig = Record<string, { source?: string; dialect?: string; path?: string; entities: string[] }> | undefined;
|
|
26
18
|
|
|
27
19
|
export interface ResolvedStorage {
|
|
28
20
|
/** Opaque handle handed to auth providers. */
|
|
29
21
|
db?: unknown;
|
|
30
|
-
|
|
22
|
+
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
31
23
|
/**
|
|
32
|
-
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
33
|
-
*
|
|
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.
|
|
24
|
+
* The source an entity's rows live in — what decides whether a frame gets a real transaction or
|
|
25
|
+
* an unwind it replays itself.
|
|
39
26
|
*/
|
|
40
27
|
sourceOf?: (entityName: string) => string;
|
|
41
28
|
/**
|
|
@@ -45,26 +32,18 @@ export interface ResolvedStorage {
|
|
|
45
32
|
dbOf?: (source: string) => unknown;
|
|
46
33
|
/** Every source that has an engine, the default one first. */
|
|
47
34
|
sources?: () => string[];
|
|
48
|
-
/**
|
|
49
|
-
|
|
35
|
+
/** Whether that source hands out a transaction — the dual of running one in it. */
|
|
36
|
+
transacts?: (source: string) => boolean;
|
|
37
|
+
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
38
|
+
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
50
39
|
/**
|
|
51
40
|
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
52
|
-
* `migrating()`.
|
|
53
|
-
* post-boot and was read in four places under the two senses.
|
|
41
|
+
* `migrating()`.
|
|
54
42
|
*/
|
|
55
43
|
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
|
-
*/
|
|
44
|
+
/** Close every engine this opened — the dual of opening them, declared by whoever did. */
|
|
64
45
|
close?: () => Promise<void>;
|
|
65
46
|
/** Raw synchronous handle, when the engine exposes one. */
|
|
66
|
-
raw?: { exec(sql: string): void };
|
|
67
|
-
dialect?: DialectName;
|
|
68
47
|
}
|
|
69
48
|
|
|
70
49
|
/** Does this config ask for persistence at all? */
|
|
@@ -73,41 +52,13 @@ export function declaresStorage(dbConf: DbConfig): boolean {
|
|
|
73
52
|
return true;
|
|
74
53
|
}
|
|
75
54
|
|
|
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
55
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
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(
|
|
56
|
+
/** The app as ONE source sees it. */
|
|
57
|
+
function viewOf(
|
|
107
58
|
app: App,
|
|
108
59
|
holds: (name: string) => boolean,
|
|
109
60
|
withAuth: boolean,
|
|
110
|
-
):
|
|
61
|
+
): SourceView {
|
|
111
62
|
const typed = app as unknown as {
|
|
112
63
|
fronds: { name: string; entities: { name: string }[] }[];
|
|
113
64
|
auth?: unknown;
|
|
@@ -120,67 +71,48 @@ function partition(
|
|
|
120
71
|
auth: withAuth ? typed.auth : undefined,
|
|
121
72
|
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
122
73
|
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
123
|
-
elsewhere: Fronds.
|
|
74
|
+
elsewhere: Fronds.hosting(typed.fronds as FrondDescriptor[]).entityNames().filter((name) => !holds(name)),
|
|
124
75
|
};
|
|
125
76
|
}
|
|
126
77
|
|
|
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
|
-
*/
|
|
78
|
+
/** Resolve the data layer. */
|
|
135
79
|
export function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage {
|
|
136
|
-
if (!declaresStorage(dbConf)) return {
|
|
80
|
+
if (!declaresStorage(dbConf)) return { storageFactory: undefined };
|
|
137
81
|
|
|
138
|
-
refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
|
|
139
82
|
const named: Record<string, Placement> = {};
|
|
140
83
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
141
|
-
|
|
142
|
-
named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
|
|
84
|
+
named[name] = { source: built(conf, `sources.${name}`), entities: conf.entities };
|
|
143
85
|
}
|
|
144
86
|
return storageFrom({
|
|
145
|
-
db:
|
|
87
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
146
88
|
sources: named,
|
|
147
89
|
});
|
|
148
90
|
}
|
|
149
91
|
|
|
150
|
-
/** One
|
|
92
|
+
/** One config entry, resolved to the adapter it names. */
|
|
93
|
+
function built(conf: Record<string, unknown>, field: string): Source {
|
|
94
|
+
const name = (conf.source as string | undefined) ?? DEFAULT_ADAPTER;
|
|
95
|
+
|
|
96
|
+
return Sources.open(name, conf as never, `${field}.source`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What a config naming no adapter means. */
|
|
100
|
+
const DEFAULT_ADAPTER = 'sql';
|
|
101
|
+
|
|
102
|
+
/** One source: what realizes it, and the entities that live there. */
|
|
151
103
|
export interface Placement {
|
|
152
|
-
|
|
104
|
+
source: Source;
|
|
153
105
|
entities: string[];
|
|
154
106
|
}
|
|
155
107
|
|
|
156
108
|
export interface DeclaredStorage {
|
|
157
109
|
/** The default source — where an entity no placement names lands. */
|
|
158
|
-
db:
|
|
110
|
+
db: Source;
|
|
159
111
|
/** The other places. Absent means one source, the way it always was. */
|
|
160
112
|
sources?: Record<string, Placement>;
|
|
161
113
|
}
|
|
162
114
|
|
|
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
|
-
*/
|
|
115
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
184
116
|
export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
185
117
|
const { db: base, sources } = declared;
|
|
186
118
|
|
|
@@ -188,9 +120,9 @@ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
|
188
120
|
// both: the rows would be read from one and written to the other, by whichever
|
|
189
121
|
// registration ran last — the same silent duplicate `remotes:` refuses one level up.
|
|
190
122
|
const home = new Map<string, string>();
|
|
191
|
-
const engines = new Map<string,
|
|
123
|
+
const engines = new Map<string, Source>();
|
|
192
124
|
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
193
|
-
engines.set(name, placement.
|
|
125
|
+
engines.set(name, placement.source);
|
|
194
126
|
for (const entity of placement.entities) {
|
|
195
127
|
const key = lowerFirst(entity);
|
|
196
128
|
const claimed = home.get(key);
|
|
@@ -214,32 +146,37 @@ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
|
214
146
|
};
|
|
215
147
|
|
|
216
148
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
149
|
+
// Opaque, and narrowed by whoever needs it — auth wants a handle, the CLI wants Kysely.
|
|
150
|
+
db: (base as { db?: unknown }).db,
|
|
151
|
+
storageFactory: (entity: any, name: string) => engineFor(name).storageFactory(entity, name),
|
|
219
152
|
sourceOf,
|
|
220
|
-
dbOf: (source) => engineOf(source)?.db,
|
|
153
|
+
dbOf: (source) => (engineOf(source) as { db?: unknown } | undefined)?.db,
|
|
221
154
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
222
|
-
|
|
155
|
+
// Asked of the source that will run the work, never of the default one: a frame whose
|
|
156
|
+
// members all live in a transactional source gets its transaction even when `db:` has
|
|
157
|
+
// none. A source that hands out none leaves the answer `false`, and a frame reads it:
|
|
158
|
+
// `boot/together.ts` compensates instead, and says which of the two it built.
|
|
159
|
+
transacts: (source) => engineOf(source)?.transacted !== undefined,
|
|
160
|
+
transacted: async (source, fn) => {
|
|
223
161
|
const engine = engineOf(source);
|
|
224
162
|
if (!engine) throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
163
|
+
if (!engine.transacted) throw new Error(`Source '${source}' hands out no transaction.`);
|
|
225
164
|
return engine.transacted(fn);
|
|
226
165
|
},
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
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.
|
|
166
|
+
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
167
|
+
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
168
|
+
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|
|
232
169
|
migrate: async (app) => {
|
|
233
|
-
await migrate(
|
|
170
|
+
await base.migrate?.(viewOf(app, (name) => !home.has(lowerFirst(name)), true));
|
|
234
171
|
for (const [name, engine] of engines) {
|
|
235
|
-
await migrate(
|
|
172
|
+
await engine.migrate?.(viewOf(app, (e) => home.get(lowerFirst(e)) === name, false));
|
|
236
173
|
}
|
|
237
174
|
},
|
|
238
|
-
// Every
|
|
239
|
-
//
|
|
175
|
+
// Every source, the default one last: a named source may hold what the default refers
|
|
176
|
+
// to, and closing in reverse of opening is the rule everywhere else.
|
|
240
177
|
close: async () => {
|
|
241
|
-
for (const engine of [...engines.values()].reverse()) await engine.
|
|
242
|
-
await base.
|
|
178
|
+
for (const engine of [...engines.values()].reverse()) await engine.close?.();
|
|
179
|
+
await base.close?.();
|
|
243
180
|
},
|
|
244
181
|
};
|
|
245
182
|
}
|