@fougere/defaults 0.5.0-alpha.1 → 0.6.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/storage.d.ts +10 -12
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +44 -40
- package/dist/storage.js.map +1 -1
- package/package.json +6 -6
- package/src/storage.ts +60 -55
package/dist/storage.d.ts
CHANGED
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
* why an engine change looked like it touched seven files.
|
|
10
10
|
*/
|
|
11
11
|
import type { App } from '@fougere/core';
|
|
12
|
-
import { type
|
|
12
|
+
import { type Source } from '@fougere/core';
|
|
13
|
+
import '@fougere/adapter-sql/sqlite';
|
|
13
14
|
/** The `db` field of fougere.config.ts, read structurally. */
|
|
14
15
|
export type DbConfig = false | 'sqlite' | {
|
|
16
|
+
source?: string;
|
|
15
17
|
dialect?: string;
|
|
16
18
|
path?: string;
|
|
17
19
|
} | undefined;
|
|
18
20
|
/** A named source and the entities it holds — the `sources` field, read structurally. */
|
|
19
21
|
export type SourcesConfig = Record<string, {
|
|
22
|
+
source?: string;
|
|
20
23
|
dialect?: string;
|
|
21
24
|
path?: string;
|
|
22
25
|
entities: string[];
|
|
@@ -24,7 +27,7 @@ export type SourcesConfig = Record<string, {
|
|
|
24
27
|
export interface ResolvedStorage {
|
|
25
28
|
/** Opaque handle handed to auth providers. */
|
|
26
29
|
db?: unknown;
|
|
27
|
-
|
|
30
|
+
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
28
31
|
/**
|
|
29
32
|
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
30
33
|
* transaction or an unwind it replays itself.
|
|
@@ -42,8 +45,8 @@ export interface ResolvedStorage {
|
|
|
42
45
|
dbOf?: (source: string) => unknown;
|
|
43
46
|
/** Every source that has an engine, the default one first. */
|
|
44
47
|
sources?: () => string[];
|
|
45
|
-
/** Run `fn` inside one transaction of that source, with
|
|
46
|
-
transacted?: <R>(source: string, fn: (
|
|
48
|
+
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
49
|
+
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
47
50
|
/**
|
|
48
51
|
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
49
52
|
* `migrating()`. It was called `afterBoot`, a word that also meant the host's own
|
|
@@ -59,11 +62,6 @@ export interface ResolvedStorage {
|
|
|
59
62
|
* ordinary rather than rare.
|
|
60
63
|
*/
|
|
61
64
|
close?: () => Promise<void>;
|
|
62
|
-
/** Raw synchronous handle, when the engine exposes one. */
|
|
63
|
-
raw?: {
|
|
64
|
-
exec(sql: string): void;
|
|
65
|
-
};
|
|
66
|
-
dialect?: DialectName;
|
|
67
65
|
}
|
|
68
66
|
/** Does this config ask for persistence at all? */
|
|
69
67
|
export declare function declaresStorage(dbConf: DbConfig): boolean;
|
|
@@ -76,14 +74,14 @@ export declare function declaresStorage(dbConf: DbConfig): boolean;
|
|
|
76
74
|
* exactly as before.
|
|
77
75
|
*/
|
|
78
76
|
export declare function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage;
|
|
79
|
-
/** One source:
|
|
77
|
+
/** One source: what realizes it, and the entities that live there. */
|
|
80
78
|
export interface Placement {
|
|
81
|
-
|
|
79
|
+
source: Source;
|
|
82
80
|
entities: string[];
|
|
83
81
|
}
|
|
84
82
|
export interface DeclaredStorage {
|
|
85
83
|
/** The default source — where an entity no placement names lands. */
|
|
86
|
-
db:
|
|
84
|
+
db: Source;
|
|
87
85
|
/** The other places. Absent means one source, the way it always was. */
|
|
88
86
|
sources?: Record<string, Placement>;
|
|
89
87
|
}
|
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;AAGzC,OAAO,EAAW,KAAK,
|
|
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,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;;;;;;;;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,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;;;;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;CAE7B;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAGzD;AAoCD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAWzF;AAyBD,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;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,eAAe,CA+DtE"}
|
package/dist/storage.js
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
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
|
-
* Only sqlite is resolvable from a NAME — its driver is a dependency here. Any other
|
|
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
12
|
/**
|
|
28
13
|
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
29
14
|
* that do not.
|
|
@@ -36,7 +21,7 @@ function refuseUnresolvable(declared, field) {
|
|
|
36
21
|
* Auth entities ride with the default source: a provider's tables are the app's own,
|
|
37
22
|
* and nothing yet lets one declare where it lives.
|
|
38
23
|
*/
|
|
39
|
-
function
|
|
24
|
+
function viewOf(app, holds, withAuth) {
|
|
40
25
|
const typed = app;
|
|
41
26
|
const fronds = typed.fronds
|
|
42
27
|
.map((frond) => ({ ...frond, entities: frond.entities.filter((entry) => holds(entry.name)) }))
|
|
@@ -46,7 +31,7 @@ function partition(app, holds, withAuth) {
|
|
|
46
31
|
auth: withAuth ? typed.auth : undefined,
|
|
47
32
|
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
48
33
|
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
49
|
-
elsewhere: Fronds.
|
|
34
|
+
elsewhere: Fronds.hosting(typed.fronds).entityNames().filter((name) => !holds(name)),
|
|
50
35
|
};
|
|
51
36
|
}
|
|
52
37
|
/**
|
|
@@ -59,18 +44,34 @@ function partition(app, holds, withAuth) {
|
|
|
59
44
|
*/
|
|
60
45
|
export function resolveStorage(dbConf, sources) {
|
|
61
46
|
if (!declaresStorage(dbConf))
|
|
62
|
-
return {
|
|
63
|
-
refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
|
|
47
|
+
return { storageFactory: undefined };
|
|
64
48
|
const named = {};
|
|
65
49
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
66
|
-
|
|
67
|
-
named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
|
|
50
|
+
named[name] = { source: built(conf, `sources.${name}`), entities: conf.entities };
|
|
68
51
|
}
|
|
69
52
|
return storageFrom({
|
|
70
|
-
db:
|
|
53
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
71
54
|
sources: named,
|
|
72
55
|
});
|
|
73
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One config entry, resolved to the adapter it names.
|
|
59
|
+
*
|
|
60
|
+
* `source:` defaults to `'sql'` — the convention a first run meets, and the reason a config
|
|
61
|
+
* saying only `db: 'sqlite'` keeps working. What the entry says BELOW that key belongs to the
|
|
62
|
+
* adapter it named, which is also the only one that can refuse it: this function knows no
|
|
63
|
+
* dialect and no driver.
|
|
64
|
+
*/
|
|
65
|
+
function built(conf, field) {
|
|
66
|
+
const name = conf.source ?? DEFAULT_ADAPTER;
|
|
67
|
+
if (!Sources.answers(name)) {
|
|
68
|
+
throw new Error(`${field}.source '${name}' is not answered — import the adapter that does. `
|
|
69
|
+
+ `This process answers ${Sources.answered().join(', ') || 'nothing yet'}.`);
|
|
70
|
+
}
|
|
71
|
+
return Sources.resolve(name, conf);
|
|
72
|
+
}
|
|
73
|
+
/** What a config naming no adapter means. */
|
|
74
|
+
const DEFAULT_ADAPTER = 'sql';
|
|
74
75
|
/**
|
|
75
76
|
* The same routing `resolveStorage` performs, over engines the CALLER built.
|
|
76
77
|
*
|
|
@@ -100,7 +101,7 @@ export function storageFrom(declared) {
|
|
|
100
101
|
const home = new Map();
|
|
101
102
|
const engines = new Map();
|
|
102
103
|
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
103
|
-
engines.set(name, placement.
|
|
104
|
+
engines.set(name, placement.source);
|
|
104
105
|
for (const entity of placement.entities) {
|
|
105
106
|
const key = lowerFirst(entity);
|
|
106
107
|
const claimed = home.get(key);
|
|
@@ -120,34 +121,37 @@ export function storageFrom(declared) {
|
|
|
120
121
|
return (source && engines.get(source)) || base;
|
|
121
122
|
};
|
|
122
123
|
return {
|
|
124
|
+
// Opaque, and narrowed by whoever needs it — auth wants a handle, the CLI wants Kysely.
|
|
123
125
|
db: base.db,
|
|
124
|
-
|
|
126
|
+
storageFactory: (entity, name) => engineFor(name).storageFactory(entity, name),
|
|
125
127
|
sourceOf,
|
|
126
128
|
dbOf: (source) => engineOf(source)?.db,
|
|
127
129
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
128
|
-
|
|
130
|
+
// A source that hands out no transaction leaves the key absent, and a frame reads the
|
|
131
|
+
// absence: `boot/together.ts` compensates instead, and says which of the two it built.
|
|
132
|
+
transacted: base.transacted ? async (source, fn) => {
|
|
129
133
|
const engine = engineOf(source);
|
|
130
134
|
if (!engine)
|
|
131
135
|
throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
136
|
+
if (!engine.transacted)
|
|
137
|
+
throw new Error(`Source '${source}' hands out no transaction.`);
|
|
132
138
|
return engine.transacted(fn);
|
|
133
|
-
},
|
|
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.
|
|
139
|
+
} : undefined,
|
|
140
|
+
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
141
|
+
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
142
|
+
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|
|
139
143
|
migrate: async (app) => {
|
|
140
|
-
await migrate(
|
|
144
|
+
await base.migrate?.(viewOf(app, (name) => !home.has(lowerFirst(name)), true));
|
|
141
145
|
for (const [name, engine] of engines) {
|
|
142
|
-
await migrate(
|
|
146
|
+
await engine.migrate?.(viewOf(app, (e) => home.get(lowerFirst(e)) === name, false));
|
|
143
147
|
}
|
|
144
148
|
},
|
|
145
|
-
// Every
|
|
146
|
-
//
|
|
149
|
+
// Every source, the default one last: a named source may hold what the default refers
|
|
150
|
+
// to, and closing in reverse of opening is the rule everywhere else.
|
|
147
151
|
close: async () => {
|
|
148
152
|
for (const engine of [...engines.values()].reverse())
|
|
149
|
-
await engine.
|
|
150
|
-
await base.
|
|
153
|
+
await engine.close?.();
|
|
154
|
+
await base.close?.();
|
|
151
155
|
},
|
|
152
156
|
};
|
|
153
157
|
}
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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
|
+
"version": "0.6.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/
|
|
34
|
-
"@fougere/
|
|
35
|
-
"@fougere/transport-http": "0.
|
|
32
|
+
"@fougere/core": "0.6.0-alpha.0",
|
|
33
|
+
"@fougere/container": "0.6.0-alpha.0",
|
|
34
|
+
"@fougere/adapter-sql": "0.6.0-alpha.0",
|
|
35
|
+
"@fougere/transport-http": "0.6.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.6.0-alpha.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "rm -rf dist && tsc",
|
package/src/storage.ts
CHANGED
|
@@ -11,23 +11,24 @@
|
|
|
11
11
|
import type { App } from '@fougere/core';
|
|
12
12
|
import { Fronds, type FrondDescriptor } from '@fougere/core';
|
|
13
13
|
import { lowerFirst } from '@fougere/core/contract';
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import { Sources, type Source, type SourceView } from '@fougere/core';
|
|
15
|
+
// Imported for its side effect: it is what makes `source: 'sql'` an answered name.
|
|
16
|
+
import '@fougere/adapter-sql/sqlite';
|
|
16
17
|
|
|
17
18
|
/** The `db` field of fougere.config.ts, read structurally. */
|
|
18
19
|
export type DbConfig =
|
|
19
20
|
| false
|
|
20
21
|
| 'sqlite'
|
|
21
|
-
| { dialect?: string; path?: string }
|
|
22
|
+
| { source?: string; dialect?: string; path?: string }
|
|
22
23
|
| undefined;
|
|
23
24
|
|
|
24
25
|
/** 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
|
+
export type SourcesConfig = Record<string, { source?: string; dialect?: string; path?: string; entities: string[] }> | undefined;
|
|
26
27
|
|
|
27
28
|
export interface ResolvedStorage {
|
|
28
29
|
/** Opaque handle handed to auth providers. */
|
|
29
30
|
db?: unknown;
|
|
30
|
-
|
|
31
|
+
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
31
32
|
/**
|
|
32
33
|
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
33
34
|
* transaction or an unwind it replays itself.
|
|
@@ -45,8 +46,8 @@ export interface ResolvedStorage {
|
|
|
45
46
|
dbOf?: (source: string) => unknown;
|
|
46
47
|
/** Every source that has an engine, the default one first. */
|
|
47
48
|
sources?: () => string[];
|
|
48
|
-
/** Run `fn` inside one transaction of that source, with
|
|
49
|
-
transacted?: <R>(source: string, fn: (
|
|
49
|
+
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
50
|
+
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
50
51
|
/**
|
|
51
52
|
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
52
53
|
* `migrating()`. It was called `afterBoot`, a word that also meant the host's own
|
|
@@ -63,8 +64,6 @@ export interface ResolvedStorage {
|
|
|
63
64
|
*/
|
|
64
65
|
close?: () => Promise<void>;
|
|
65
66
|
/** Raw synchronous handle, when the engine exposes one. */
|
|
66
|
-
raw?: { exec(sql: string): void };
|
|
67
|
-
dialect?: DialectName;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/** Does this config ask for persistence at all? */
|
|
@@ -73,23 +72,6 @@ export function declaresStorage(dbConf: DbConfig): boolean {
|
|
|
73
72
|
return true;
|
|
74
73
|
}
|
|
75
74
|
|
|
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
75
|
|
|
94
76
|
/**
|
|
95
77
|
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
@@ -103,11 +85,11 @@ function refuseUnresolvable(declared: string | undefined, field: string): void {
|
|
|
103
85
|
* Auth entities ride with the default source: a provider's tables are the app's own,
|
|
104
86
|
* and nothing yet lets one declare where it lives.
|
|
105
87
|
*/
|
|
106
|
-
function
|
|
88
|
+
function viewOf(
|
|
107
89
|
app: App,
|
|
108
90
|
holds: (name: string) => boolean,
|
|
109
91
|
withAuth: boolean,
|
|
110
|
-
):
|
|
92
|
+
): SourceView {
|
|
111
93
|
const typed = app as unknown as {
|
|
112
94
|
fronds: { name: string; entities: { name: string }[] }[];
|
|
113
95
|
auth?: unknown;
|
|
@@ -120,7 +102,7 @@ function partition(
|
|
|
120
102
|
auth: withAuth ? typed.auth : undefined,
|
|
121
103
|
// Lifted, because this function reads its app structurally on purpose — a caller
|
|
122
104
|
// may hand it a shape that is app-LIKE, and the question is still the same one.
|
|
123
|
-
elsewhere: Fronds.
|
|
105
|
+
elsewhere: Fronds.hosting(typed.fronds as FrondDescriptor[]).entityNames().filter((name) => !holds(name)),
|
|
124
106
|
};
|
|
125
107
|
}
|
|
126
108
|
|
|
@@ -133,29 +115,50 @@ function partition(
|
|
|
133
115
|
* exactly as before.
|
|
134
116
|
*/
|
|
135
117
|
export function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig): ResolvedStorage {
|
|
136
|
-
if (!declaresStorage(dbConf)) return {
|
|
118
|
+
if (!declaresStorage(dbConf)) return { storageFactory: undefined };
|
|
137
119
|
|
|
138
|
-
refuseUnresolvable(typeof dbConf === 'object' ? dbConf.dialect : (dbConf || undefined), 'db.dialect');
|
|
139
120
|
const named: Record<string, Placement> = {};
|
|
140
121
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
141
|
-
|
|
142
|
-
named[name] = { setup: setupSqlite({ path: conf.path }), entities: conf.entities };
|
|
122
|
+
named[name] = { source: built(conf, `sources.${name}`), entities: conf.entities };
|
|
143
123
|
}
|
|
144
124
|
return storageFrom({
|
|
145
|
-
db:
|
|
125
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
146
126
|
sources: named,
|
|
147
127
|
});
|
|
148
128
|
}
|
|
149
129
|
|
|
150
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* One config entry, resolved to the adapter it names.
|
|
132
|
+
*
|
|
133
|
+
* `source:` defaults to `'sql'` — the convention a first run meets, and the reason a config
|
|
134
|
+
* saying only `db: 'sqlite'` keeps working. What the entry says BELOW that key belongs to the
|
|
135
|
+
* adapter it named, which is also the only one that can refuse it: this function knows no
|
|
136
|
+
* dialect and no driver.
|
|
137
|
+
*/
|
|
138
|
+
function built(conf: Record<string, unknown>, field: string): Source {
|
|
139
|
+
const name = (conf.source as string | undefined) ?? DEFAULT_ADAPTER;
|
|
140
|
+
if (!Sources.answers(name)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`${field}.source '${name}' is not answered — import the adapter that does. `
|
|
143
|
+
+ `This process answers ${Sources.answered().join(', ') || 'nothing yet'}.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Sources.resolve(name, conf as never);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What a config naming no adapter means. */
|
|
151
|
+
const DEFAULT_ADAPTER = 'sql';
|
|
152
|
+
|
|
153
|
+
/** One source: what realizes it, and the entities that live there. */
|
|
151
154
|
export interface Placement {
|
|
152
|
-
|
|
155
|
+
source: Source;
|
|
153
156
|
entities: string[];
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
export interface DeclaredStorage {
|
|
157
160
|
/** The default source — where an entity no placement names lands. */
|
|
158
|
-
db:
|
|
161
|
+
db: Source;
|
|
159
162
|
/** The other places. Absent means one source, the way it always was. */
|
|
160
163
|
sources?: Record<string, Placement>;
|
|
161
164
|
}
|
|
@@ -188,9 +191,9 @@ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
|
188
191
|
// both: the rows would be read from one and written to the other, by whichever
|
|
189
192
|
// registration ran last — the same silent duplicate `remotes:` refuses one level up.
|
|
190
193
|
const home = new Map<string, string>();
|
|
191
|
-
const engines = new Map<string,
|
|
194
|
+
const engines = new Map<string, Source>();
|
|
192
195
|
for (const [name, placement] of Object.entries(sources ?? {})) {
|
|
193
|
-
engines.set(name, placement.
|
|
196
|
+
engines.set(name, placement.source);
|
|
194
197
|
for (const entity of placement.entities) {
|
|
195
198
|
const key = lowerFirst(entity);
|
|
196
199
|
const claimed = home.get(key);
|
|
@@ -214,32 +217,34 @@ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
|
214
217
|
};
|
|
215
218
|
|
|
216
219
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
220
|
+
// Opaque, and narrowed by whoever needs it — auth wants a handle, the CLI wants Kysely.
|
|
221
|
+
db: (base as { db?: unknown }).db,
|
|
222
|
+
storageFactory: (entity: any, name: string) => engineFor(name).storageFactory(entity, name),
|
|
219
223
|
sourceOf,
|
|
220
|
-
dbOf: (source) => engineOf(source)?.db,
|
|
224
|
+
dbOf: (source) => (engineOf(source) as { db?: unknown } | undefined)?.db,
|
|
221
225
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
222
|
-
|
|
226
|
+
// A source that hands out no transaction leaves the key absent, and a frame reads the
|
|
227
|
+
// absence: `boot/together.ts` compensates instead, and says which of the two it built.
|
|
228
|
+
transacted: base.transacted ? async (source, fn) => {
|
|
223
229
|
const engine = engineOf(source);
|
|
224
230
|
if (!engine) throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
231
|
+
if (!engine.transacted) throw new Error(`Source '${source}' hands out no transaction.`);
|
|
225
232
|
return engine.transacted(fn);
|
|
226
|
-
},
|
|
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.
|
|
233
|
+
} : undefined,
|
|
234
|
+
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
235
|
+
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
236
|
+
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|
|
232
237
|
migrate: async (app) => {
|
|
233
|
-
await migrate(
|
|
238
|
+
await base.migrate?.(viewOf(app, (name) => !home.has(lowerFirst(name)), true));
|
|
234
239
|
for (const [name, engine] of engines) {
|
|
235
|
-
await migrate(
|
|
240
|
+
await engine.migrate?.(viewOf(app, (e) => home.get(lowerFirst(e)) === name, false));
|
|
236
241
|
}
|
|
237
242
|
},
|
|
238
|
-
// Every
|
|
239
|
-
//
|
|
243
|
+
// Every source, the default one last: a named source may hold what the default refers
|
|
244
|
+
// to, and closing in reverse of opening is the rule everywhere else.
|
|
240
245
|
close: async () => {
|
|
241
|
-
for (const engine of [...engines.values()].reverse()) await engine.
|
|
242
|
-
await base.
|
|
246
|
+
for (const engine of [...engines.values()].reverse()) await engine.close?.();
|
|
247
|
+
await base.close?.();
|
|
243
248
|
},
|
|
244
249
|
};
|
|
245
250
|
}
|