@fougere/defaults 0.6.0-alpha.0 → 0.8.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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storage.d.ts +20 -55
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +46 -57
- package/dist/storage.js.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +2 -11
- package/src/storage.ts +66 -89
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
CHANGED
|
@@ -18,7 +18,7 @@ export async function bootAppFromConfig(root, opts = {}) {
|
|
|
18
18
|
remotes: useRemotes ? remotes : undefined,
|
|
19
19
|
remoteTransport: useRemotes ? (url) => createHttpTransport(url) : undefined,
|
|
20
20
|
// One resolver, one place that knows a storage package.
|
|
21
|
-
db: (cfg) => resolveStorage(cfg.db, cfg.sources),
|
|
21
|
+
db: (cfg) => resolveStorage(cfg.db, cfg.sources, root),
|
|
22
22
|
extensions: opts.extensions,
|
|
23
23
|
});
|
|
24
24
|
}
|
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,EAAE,IAAI,CAAC;QACtG,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC,CAAC;AACL,CAAC"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
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 Source } from '@fougere/core';
|
|
3
|
+
import { type Constraint, type Source } from '@fougere/core';
|
|
13
4
|
import '@fougere/adapter-sql/sqlite';
|
|
14
5
|
/** The `db` field of fougere.config.ts, read structurally. */
|
|
15
6
|
export type DbConfig = false | 'sqlite' | {
|
|
@@ -29,13 +20,8 @@ export interface ResolvedStorage {
|
|
|
29
20
|
db?: unknown;
|
|
30
21
|
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
31
22
|
/**
|
|
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.
|
|
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.
|
|
39
25
|
*/
|
|
40
26
|
sourceOf?: (entityName: string) => string;
|
|
41
27
|
/**
|
|
@@ -45,35 +31,34 @@ export interface ResolvedStorage {
|
|
|
45
31
|
dbOf?: (source: string) => unknown;
|
|
46
32
|
/** Every source that has an engine, the default one first. */
|
|
47
33
|
sources?: () => string[];
|
|
34
|
+
/** Whether that source hands out a transaction — the dual of running one in it. */
|
|
35
|
+
transacts?: (source: string) => boolean;
|
|
36
|
+
/** Whether that source refuses a constraint at the rows — the dual of an entity declaring one. */
|
|
37
|
+
enforces?: (source: string, constraint: Constraint) => boolean;
|
|
48
38
|
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
49
39
|
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
50
40
|
/**
|
|
51
41
|
* 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.
|
|
42
|
+
* `migrating()`.
|
|
54
43
|
*/
|
|
55
44
|
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
|
-
*/
|
|
45
|
+
/** Close every engine this opened — the dual of opening them, declared by whoever did. */
|
|
64
46
|
close?: () => Promise<void>;
|
|
65
47
|
}
|
|
66
48
|
/** Does this config ask for persistence at all? */
|
|
67
49
|
export declare function declaresStorage(dbConf: DbConfig): boolean;
|
|
50
|
+
/** Resolve the data layer. */
|
|
51
|
+
export declare function resolveStorage(dbConf: DbConfig, sources?: SourcesConfig,
|
|
68
52
|
/**
|
|
69
|
-
*
|
|
70
|
-
* persistence of its own — the caller decides what to fall back to.
|
|
53
|
+
* Where a relative `path:` is counted from — the directory of the config that named it.
|
|
71
54
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
55
|
+
* Absent, it is counted from the current directory, which is where an app in a
|
|
56
|
+
* workspace parts ways with the file that configured it: measured, `apps/nuxt` ran from
|
|
57
|
+
* its own directory and made a SECOND, empty database beside itself while the workspace
|
|
58
|
+
* held the real one. Nothing was said; the additive migration created every table, the
|
|
59
|
+
* seeds ran, and the app served an empty domain with a green boot.
|
|
75
60
|
*/
|
|
76
|
-
|
|
61
|
+
root?: string): ResolvedStorage;
|
|
77
62
|
/** One source: what realizes it, and the entities that live there. */
|
|
78
63
|
export interface Placement {
|
|
79
64
|
source: Source;
|
|
@@ -85,26 +70,6 @@ export interface DeclaredStorage {
|
|
|
85
70
|
/** The other places. Absent means one source, the way it always was. */
|
|
86
71
|
sources?: Record<string, Placement>;
|
|
87
72
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The same routing `resolveStorage` performs, over engines the CALLER built.
|
|
90
|
-
*
|
|
91
|
-
* `resolveStorage` reads a config file, and a config file cannot hold a live Kysely
|
|
92
|
-
* dialect — so it can only ever resolve sqlite, the one driver this package depends
|
|
93
|
-
* on. That was fine while `db:` was alone, because the escape hatch was to abandon
|
|
94
|
-
* the convention entirely and hand `configureFougere` your own factory. With several
|
|
95
|
-
* sources that stopped being an escape: a user wanting Postgres for ONE of them had
|
|
96
|
-
* to re-implement the routing and the per-source migration to keep the other.
|
|
97
|
-
*
|
|
98
|
-
* So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
|
|
99
|
-
* stays there; placing entities and migrating each source is engine-agnostic and
|
|
100
|
-
* lives here, where any `Setup` is welcome:
|
|
101
|
-
*
|
|
102
|
-
* ```ts
|
|
103
|
-
* configureFougere(storageFrom({
|
|
104
|
-
* db: setupSqlite({ path: '.data/app.db' }),
|
|
105
|
-
* sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
|
|
106
|
-
* }));
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
73
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
109
74
|
export declare function storageFrom(declared: DeclaredStorage): ResolvedStorage;
|
|
110
75
|
//# 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;AAMzC,OAAO,EAAW,KAAK,UAAU,EAAE,KAAK,MAAM,EAAmB,MAAM,eAAe,CAAC;AAEvF,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,kGAAkG;IAClG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,KAAK,OAAO,CAAC;IAC/D,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,CAC5B,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,aAAa;AACvB;;;;;;;;GAQG;AACH,IAAI,CAAC,EAAE,MAAM,GACZ,eAAe,CAWjB;AAoCD,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,CAqEtE"}
|
package/dist/storage.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Fronds } from '@fougere/core';
|
|
2
2
|
import { lowerFirst } from '@fougere/core/contract';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
5
|
import { Sources } from '@fougere/core';
|
|
4
6
|
// Imported for its side effect: it is what makes `source: 'sql'` an answered name.
|
|
5
7
|
import '@fougere/adapter-sql/sqlite';
|
|
@@ -9,18 +11,7 @@ export function declaresStorage(dbConf) {
|
|
|
9
11
|
return false;
|
|
10
12
|
return true;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
13
|
-
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
14
|
-
* that do not.
|
|
15
|
-
*
|
|
16
|
-
* The second half is what lets the DDL stop lying. A batch holding every entity could
|
|
17
|
-
* never tell a cross-source target from a typo, so it derived a table name and emitted
|
|
18
|
-
* a foreign key against a table that may not exist. Cut per source, `elsewhere` says
|
|
19
|
-
* which misses are legitimate — and a target in neither list is a mistake, out loud.
|
|
20
|
-
*
|
|
21
|
-
* Auth entities ride with the default source: a provider's tables are the app's own,
|
|
22
|
-
* and nothing yet lets one declare where it lives.
|
|
23
|
-
*/
|
|
14
|
+
/** The app as ONE source sees it. */
|
|
24
15
|
function viewOf(app, holds, withAuth) {
|
|
25
16
|
const typed = app;
|
|
26
17
|
const fronds = typed.fronds
|
|
@@ -34,65 +25,57 @@ function viewOf(app, holds, withAuth) {
|
|
|
34
25
|
elsewhere: Fronds.hosting(typed.fronds).entityNames().filter((name) => !holds(name)),
|
|
35
26
|
};
|
|
36
27
|
}
|
|
28
|
+
/** Resolve the data layer. */
|
|
29
|
+
export function resolveStorage(dbConf, sources,
|
|
37
30
|
/**
|
|
38
|
-
*
|
|
39
|
-
* persistence of its own — the caller decides what to fall back to.
|
|
31
|
+
* Where a relative `path:` is counted from — the directory of the config that named it.
|
|
40
32
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
33
|
+
* Absent, it is counted from the current directory, which is where an app in a
|
|
34
|
+
* workspace parts ways with the file that configured it: measured, `apps/nuxt` ran from
|
|
35
|
+
* its own directory and made a SECOND, empty database beside itself while the workspace
|
|
36
|
+
* held the real one. Nothing was said; the additive migration created every table, the
|
|
37
|
+
* seeds ran, and the app served an empty domain with a green boot.
|
|
44
38
|
*/
|
|
45
|
-
|
|
39
|
+
root) {
|
|
46
40
|
if (!declaresStorage(dbConf))
|
|
47
41
|
return { storageFactory: undefined };
|
|
48
42
|
const named = {};
|
|
49
43
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
50
|
-
named[name] = { source: built(conf, `sources.${name}
|
|
44
|
+
named[name] = { source: built(conf, `sources.${name}`, root), entities: conf.entities };
|
|
51
45
|
}
|
|
52
46
|
return storageFrom({
|
|
53
|
-
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
47
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db', root),
|
|
54
48
|
sources: named,
|
|
55
49
|
});
|
|
56
50
|
}
|
|
51
|
+
/** One config entry, resolved to the adapter it names. */
|
|
52
|
+
function built(conf, field, root) {
|
|
53
|
+
const name = conf.source ?? DEFAULT_ADAPTER;
|
|
54
|
+
return Sources.open(name, anchored(conf, root), `${field}.source`);
|
|
55
|
+
}
|
|
57
56
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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.
|
|
57
|
+
* A relative path, counted from the config that named it. `:memory:` names no file and an
|
|
58
|
+
* absolute path already says where it is — both pass through untouched, which is what
|
|
59
|
+
* keeps this from being a second reader of what a path means.
|
|
64
60
|
*/
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
function anchored(conf, root) {
|
|
62
|
+
const path = conf.path;
|
|
63
|
+
if (!root || typeof path !== 'string' || path === ':memory:' || isAbsolute(path))
|
|
64
|
+
return conf;
|
|
65
|
+
const anchoredPath = resolve(root, path);
|
|
66
|
+
// Anchoring MOVES a database that a deployment has been running against, and the driver
|
|
67
|
+
// would create the new one without a word: additive migration, seeds, an empty domain
|
|
68
|
+
// and a green boot. So the one case where the two disagree is named.
|
|
69
|
+
const beside = resolve(process.cwd(), path);
|
|
70
|
+
if (beside !== anchoredPath && existsSync(beside) && !existsSync(anchoredPath)) {
|
|
71
|
+
process.emitWarning(`${path} resolves to ${anchoredPath} beside the config that named it, and a database `
|
|
72
|
+
+ `already sits at ${beside}. Fougere opens the first; move the file, or write the path you mean.`);
|
|
70
73
|
}
|
|
71
|
-
return
|
|
74
|
+
return { ...conf, path: anchoredPath };
|
|
72
75
|
}
|
|
73
76
|
/** What a config naming no adapter means. */
|
|
74
77
|
const DEFAULT_ADAPTER = 'sql';
|
|
75
|
-
/**
|
|
76
|
-
* The same routing `resolveStorage` performs, over engines the CALLER built.
|
|
77
|
-
*
|
|
78
|
-
* `resolveStorage` reads a config file, and a config file cannot hold a live Kysely
|
|
79
|
-
* dialect — so it can only ever resolve sqlite, the one driver this package depends
|
|
80
|
-
* on. That was fine while `db:` was alone, because the escape hatch was to abandon
|
|
81
|
-
* the convention entirely and hand `configureFougere` your own factory. With several
|
|
82
|
-
* sources that stopped being an escape: a user wanting Postgres for ONE of them had
|
|
83
|
-
* to re-implement the routing and the per-source migration to keep the other.
|
|
84
|
-
*
|
|
85
|
-
* So the two jobs are separated. Resolving a NAME into an engine is sqlite-only and
|
|
86
|
-
* stays there; placing entities and migrating each source is engine-agnostic and
|
|
87
|
-
* lives here, where any `Setup` is welcome:
|
|
88
|
-
*
|
|
89
|
-
* ```ts
|
|
90
|
-
* configureFougere(storageFrom({
|
|
91
|
-
* db: setupSqlite({ path: '.data/app.db' }),
|
|
92
|
-
* sources: { legacy: { setup: setupKysely(pgDialect, 'postgres'), entities: ['Book'] } },
|
|
93
|
-
* }));
|
|
94
|
-
* ```
|
|
95
|
-
*/
|
|
78
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
96
79
|
export function storageFrom(declared) {
|
|
97
80
|
const { db: base, sources } = declared;
|
|
98
81
|
// Where each named entity lives. An entity claimed by two sources is refused naming
|
|
@@ -127,16 +110,22 @@ export function storageFrom(declared) {
|
|
|
127
110
|
sourceOf,
|
|
128
111
|
dbOf: (source) => engineOf(source)?.db,
|
|
129
112
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
113
|
+
// Asked of the source that will run the work, never of the default one: a frame whose
|
|
114
|
+
// members all live in a transactional source gets its transaction even when `db:` has
|
|
115
|
+
// none. A source that hands out none leaves the answer `false`, and a frame reads it:
|
|
116
|
+
// `boot/together.ts` compensates instead, and says which of the two it built.
|
|
117
|
+
transacts: (source) => engineOf(source)?.transacted !== undefined,
|
|
118
|
+
// Absent means the judge is alone with it: a source that keeps no constraint of its own
|
|
119
|
+
// says nothing, the way one that hands out no transaction says nothing.
|
|
120
|
+
enforces: (source, constraint) => engineOf(source)?.enforces?.includes(constraint) ?? false,
|
|
121
|
+
transacted: async (source, fn) => {
|
|
133
122
|
const engine = engineOf(source);
|
|
134
123
|
if (!engine)
|
|
135
124
|
throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
136
125
|
if (!engine.transacted)
|
|
137
126
|
throw new Error(`Source '${source}' hands out no transaction.`);
|
|
138
127
|
return engine.transacted(fn);
|
|
139
|
-
}
|
|
128
|
+
},
|
|
140
129
|
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
141
130
|
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
142
131
|
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|
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":"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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.8.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/
|
|
33
|
-
"@fougere/
|
|
34
|
-
"@fougere/
|
|
35
|
-
"@fougere/
|
|
32
|
+
"@fougere/adapter-sql": "0.8.0-alpha.0",
|
|
33
|
+
"@fougere/core": "0.8.0-alpha.0",
|
|
34
|
+
"@fougere/transport-http": "0.8.0-alpha.0",
|
|
35
|
+
"@fougere/container": "0.8.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.8.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';
|
|
@@ -48,7 +39,7 @@ export async function bootAppFromConfig(root: string, opts: BootAppOptions = {})
|
|
|
48
39
|
remotes: useRemotes ? remotes : undefined,
|
|
49
40
|
remoteTransport: useRemotes ? (url) => createHttpTransport(url) : undefined,
|
|
50
41
|
// One resolver, one place that knows a storage package.
|
|
51
|
-
db: (cfg) => resolveStorage(cfg.db as DbConfig, (cfg as { sources?: unknown }).sources as never),
|
|
42
|
+
db: (cfg) => resolveStorage(cfg.db as DbConfig, (cfg as { sources?: unknown }).sources as never, root),
|
|
52
43
|
extensions: opts.extensions,
|
|
53
44
|
});
|
|
54
45
|
}
|
package/src/storage.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
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 {
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { Sources, type Constraint, type Source, type SourceView } from '@fougere/core';
|
|
15
9
|
// Imported for its side effect: it is what makes `source: 'sql'` an answered name.
|
|
16
10
|
import '@fougere/adapter-sql/sqlite';
|
|
17
11
|
|
|
@@ -30,13 +24,8 @@ export interface ResolvedStorage {
|
|
|
30
24
|
db?: unknown;
|
|
31
25
|
storageFactory: ((entity: any, name: string) => any) | undefined;
|
|
32
26
|
/**
|
|
33
|
-
* The source an entity's rows live in — what decides whether a frame gets a real
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Absent when the caller built a `ResolvedStorage` by hand: there is then one factory
|
|
37
|
-
* and no routing, so every entity IS on one engine — but nothing here can reach into it
|
|
38
|
-
* for a transaction, and a frame falls back to compensating. Not knowing and promising
|
|
39
|
-
* atomicity are two different things.
|
|
27
|
+
* The source an entity's rows live in — what decides whether a frame gets a real transaction or
|
|
28
|
+
* an unwind it replays itself.
|
|
40
29
|
*/
|
|
41
30
|
sourceOf?: (entityName: string) => string;
|
|
42
31
|
/**
|
|
@@ -46,22 +35,18 @@ export interface ResolvedStorage {
|
|
|
46
35
|
dbOf?: (source: string) => unknown;
|
|
47
36
|
/** Every source that has an engine, the default one first. */
|
|
48
37
|
sources?: () => string[];
|
|
38
|
+
/** Whether that source hands out a transaction — the dual of running one in it. */
|
|
39
|
+
transacts?: (source: string) => boolean;
|
|
40
|
+
/** Whether that source refuses a constraint at the rows — the dual of an entity declaring one. */
|
|
41
|
+
enforces?: (source: string, constraint: Constraint) => boolean;
|
|
49
42
|
/** Run `fn` inside one transaction of that source, with a storage factory bound to it. */
|
|
50
43
|
transacted?: <R>(source: string, fn: (storageFactory: (entity: any, name: string) => any) => Promise<R>) => Promise<R>;
|
|
51
44
|
/**
|
|
52
45
|
* Brings the schema up to date once the app is scanned — the storage's `up`, handed to
|
|
53
|
-
* `migrating()`.
|
|
54
|
-
* post-boot and was read in four places under the two senses.
|
|
46
|
+
* `migrating()`.
|
|
55
47
|
*/
|
|
56
48
|
migrate?: (app: App) => Promise<void> | void;
|
|
57
|
-
/**
|
|
58
|
-
* Close every engine this opened — the dual of opening them, declared by whoever did.
|
|
59
|
-
*
|
|
60
|
-
* `boot()` calls the factory that lands here, so `boot()` is what owns closing it: a
|
|
61
|
-
* container disposes what IT built, and this connection was handed in. Without this
|
|
62
|
-
* the pool of a discarded app stayed open, which is what turning the ring makes
|
|
63
|
-
* ordinary rather than rare.
|
|
64
|
-
*/
|
|
49
|
+
/** Close every engine this opened — the dual of opening them, declared by whoever did. */
|
|
65
50
|
close?: () => Promise<void>;
|
|
66
51
|
/** Raw synchronous handle, when the engine exposes one. */
|
|
67
52
|
}
|
|
@@ -73,18 +58,7 @@ export function declaresStorage(dbConf: DbConfig): boolean {
|
|
|
73
58
|
}
|
|
74
59
|
|
|
75
60
|
|
|
76
|
-
/**
|
|
77
|
-
* The app as ONE source sees it: the entities that live there, and the names of those
|
|
78
|
-
* that do not.
|
|
79
|
-
*
|
|
80
|
-
* The second half is what lets the DDL stop lying. A batch holding every entity could
|
|
81
|
-
* never tell a cross-source target from a typo, so it derived a table name and emitted
|
|
82
|
-
* a foreign key against a table that may not exist. Cut per source, `elsewhere` says
|
|
83
|
-
* which misses are legitimate — and a target in neither list is a mistake, out loud.
|
|
84
|
-
*
|
|
85
|
-
* Auth entities ride with the default source: a provider's tables are the app's own,
|
|
86
|
-
* and nothing yet lets one declare where it lives.
|
|
87
|
-
*/
|
|
61
|
+
/** The app as ONE source sees it. */
|
|
88
62
|
function viewOf(
|
|
89
63
|
app: App,
|
|
90
64
|
holds: (name: string) => boolean,
|
|
@@ -106,45 +80,62 @@ function viewOf(
|
|
|
106
80
|
};
|
|
107
81
|
}
|
|
108
82
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
83
|
+
/** Resolve the data layer. */
|
|
84
|
+
export function resolveStorage(
|
|
85
|
+
dbConf: DbConfig,
|
|
86
|
+
sources?: SourcesConfig,
|
|
87
|
+
/**
|
|
88
|
+
* Where a relative `path:` is counted from — the directory of the config that named it.
|
|
89
|
+
*
|
|
90
|
+
* Absent, it is counted from the current directory, which is where an app in a
|
|
91
|
+
* workspace parts ways with the file that configured it: measured, `apps/nuxt` ran from
|
|
92
|
+
* its own directory and made a SECOND, empty database beside itself while the workspace
|
|
93
|
+
* held the real one. Nothing was said; the additive migration created every table, the
|
|
94
|
+
* seeds ran, and the app served an empty domain with a green boot.
|
|
95
|
+
*/
|
|
96
|
+
root?: string,
|
|
97
|
+
): ResolvedStorage {
|
|
118
98
|
if (!declaresStorage(dbConf)) return { storageFactory: undefined };
|
|
119
99
|
|
|
120
100
|
const named: Record<string, Placement> = {};
|
|
121
101
|
for (const [name, conf] of Object.entries(sources ?? {})) {
|
|
122
|
-
named[name] = { source: built(conf, `sources.${name}
|
|
102
|
+
named[name] = { source: built(conf, `sources.${name}`, root), entities: conf.entities };
|
|
123
103
|
}
|
|
124
104
|
return storageFrom({
|
|
125
|
-
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db'),
|
|
105
|
+
db: built(typeof dbConf === 'object' ? dbConf : {}, 'db', root),
|
|
126
106
|
sources: named,
|
|
127
107
|
});
|
|
128
108
|
}
|
|
129
109
|
|
|
110
|
+
/** One config entry, resolved to the adapter it names. */
|
|
111
|
+
function built(conf: Record<string, unknown>, field: string, root?: string): Source {
|
|
112
|
+
const name = (conf.source as string | undefined) ?? DEFAULT_ADAPTER;
|
|
113
|
+
|
|
114
|
+
return Sources.open(name, anchored(conf, root) as never, `${field}.source`);
|
|
115
|
+
}
|
|
116
|
+
|
|
130
117
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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.
|
|
118
|
+
* A relative path, counted from the config that named it. `:memory:` names no file and an
|
|
119
|
+
* absolute path already says where it is — both pass through untouched, which is what
|
|
120
|
+
* keeps this from being a second reader of what a path means.
|
|
137
121
|
*/
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
122
|
+
function anchored(conf: Record<string, unknown>, root?: string): Record<string, unknown> {
|
|
123
|
+
const path = conf.path;
|
|
124
|
+
if (!root || typeof path !== 'string' || path === ':memory:' || isAbsolute(path)) return conf;
|
|
125
|
+
|
|
126
|
+
const anchoredPath = resolve(root, path);
|
|
127
|
+
// Anchoring MOVES a database that a deployment has been running against, and the driver
|
|
128
|
+
// would create the new one without a word: additive migration, seeds, an empty domain
|
|
129
|
+
// and a green boot. So the one case where the two disagree is named.
|
|
130
|
+
const beside = resolve(process.cwd(), path);
|
|
131
|
+
if (beside !== anchoredPath && existsSync(beside) && !existsSync(anchoredPath)) {
|
|
132
|
+
process.emitWarning(
|
|
133
|
+
`${path} resolves to ${anchoredPath} beside the config that named it, and a database `
|
|
134
|
+
+ `already sits at ${beside}. Fougere opens the first; move the file, or write the path you mean.`,
|
|
144
135
|
);
|
|
145
136
|
}
|
|
146
137
|
|
|
147
|
-
return
|
|
138
|
+
return { ...conf, path: anchoredPath };
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
/** What a config naming no adapter means. */
|
|
@@ -163,27 +154,7 @@ export interface DeclaredStorage {
|
|
|
163
154
|
sources?: Record<string, Placement>;
|
|
164
155
|
}
|
|
165
156
|
|
|
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
|
-
*/
|
|
157
|
+
/** The same routing `resolveStorage` performs, over engines the CALLER built. */
|
|
187
158
|
export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
188
159
|
const { db: base, sources } = declared;
|
|
189
160
|
|
|
@@ -223,14 +194,20 @@ export function storageFrom(declared: DeclaredStorage): ResolvedStorage {
|
|
|
223
194
|
sourceOf,
|
|
224
195
|
dbOf: (source) => (engineOf(source) as { db?: unknown } | undefined)?.db,
|
|
225
196
|
sources: () => [DEFAULT, ...engines.keys()],
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
197
|
+
// Asked of the source that will run the work, never of the default one: a frame whose
|
|
198
|
+
// members all live in a transactional source gets its transaction even when `db:` has
|
|
199
|
+
// none. A source that hands out none leaves the answer `false`, and a frame reads it:
|
|
200
|
+
// `boot/together.ts` compensates instead, and says which of the two it built.
|
|
201
|
+
transacts: (source) => engineOf(source)?.transacted !== undefined,
|
|
202
|
+
// Absent means the judge is alone with it: a source that keeps no constraint of its own
|
|
203
|
+
// says nothing, the way one that hands out no transaction says nothing.
|
|
204
|
+
enforces: (source, constraint) => engineOf(source)?.enforces?.includes(constraint) ?? false,
|
|
205
|
+
transacted: async (source, fn) => {
|
|
229
206
|
const engine = engineOf(source);
|
|
230
207
|
if (!engine) throw new Error(`No source named '${source}' — declared sources are ${[DEFAULT, ...engines.keys()].join(', ')}.`);
|
|
231
208
|
if (!engine.transacted) throw new Error(`Source '${source}' hands out no transaction.`);
|
|
232
209
|
return engine.transacted(fn);
|
|
233
|
-
}
|
|
210
|
+
},
|
|
234
211
|
// One pass per source, each seeing only its own entities and the NAMES of the others —
|
|
235
212
|
// which is what makes a cross-source `ref()` a miss rather than a constraint against a
|
|
236
213
|
// stranger. What a pass DOES is the source's own: it knows its engine, this does not.
|