@fougere/defaults 0.2.0-alpha.2 → 0.4.0-alpha.0

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