@fonderie/config 1.0.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index-v2hLmm2m.d.cts +32 -0
- package/dist/index-v2hLmm2m.d.ts +32 -0
- package/dist/index.cjs +209 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +174 -0
- package/dist/index.js.map +1 -0
- package/dist/middlewares/index.cjs +51 -0
- package/dist/middlewares/index.cjs.map +1 -0
- package/dist/middlewares/index.d.cts +3 -0
- package/dist/middlewares/index.d.ts +3 -0
- package/dist/middlewares/index.js +23 -0
- package/dist/middlewares/index.js.map +1 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_config.sql +13 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +14 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fonderie, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @fonderie/config
|
|
2
|
+
|
|
3
|
+
DB-backed feature flags and remote config: per-environment overrides and
|
|
4
|
+
TTL-cached snapshots, changeable at runtime without a deploy.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @fonderie/config
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { FonderieApp, defineConfig } from '@fonderie/core';
|
|
16
|
+
import { RemoteConfigModule } from '@fonderie/config';
|
|
17
|
+
|
|
18
|
+
const app = await new FonderieApp(defineConfig({}))
|
|
19
|
+
.register(new RemoteConfigModule())
|
|
20
|
+
.boot();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { configContextMiddleware, getConfig } from '@fonderie/config';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
CRUD services (`listConfigEntries`, `setConfigEntry`, …) are exported for
|
|
28
|
+
building an admin surface on top.
|
|
29
|
+
|
|
30
|
+
## Why this exists
|
|
31
|
+
|
|
32
|
+
You've shipped this plumbing before — auth, teams, billing, messaging —
|
|
33
|
+
and the next project will ask for it again. Fonderie packages it once:
|
|
34
|
+
plain TypeScript modules for
|
|
35
|
+
[`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
|
|
36
|
+
PostgreSQL-backed, self-hosted, MIT. No external control plane, no
|
|
37
|
+
per-seat anything. Register the modules you need; skip the ones you don't.
|
|
38
|
+
|
|
39
|
+
**This package owns** how behavior changes without a deploy. Feature flags and remote
|
|
40
|
+
config read live by the other bricks.
|
|
41
|
+
|
|
42
|
+
Browse the whole set at
|
|
43
|
+
[fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
|
|
44
|
+
[@fonderiejs](https://x.com/fonderiejs)
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT © Fonderie, Inc.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Middleware } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
+
|
|
4
|
+
interface IRemoteConfigOptions {
|
|
5
|
+
ttl?: number;
|
|
6
|
+
environment?: string;
|
|
7
|
+
table?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare class RemoteConfigManager {
|
|
11
|
+
private store;
|
|
12
|
+
private snapshot;
|
|
13
|
+
private interval;
|
|
14
|
+
private environment;
|
|
15
|
+
private ttl;
|
|
16
|
+
private table;
|
|
17
|
+
constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
|
|
18
|
+
boot(): Promise<void>;
|
|
19
|
+
stop(): void;
|
|
20
|
+
get<T>(key: string, fallback: T): T;
|
|
21
|
+
all(): Record<string, unknown>;
|
|
22
|
+
refresh(): Promise<void>;
|
|
23
|
+
isStale(): boolean;
|
|
24
|
+
}
|
|
25
|
+
declare const CONFIG_MANAGER_KEY = "fonderie.config.snapshot";
|
|
26
|
+
|
|
27
|
+
declare function configContextMiddleware(manager: RemoteConfigManager): Middleware;
|
|
28
|
+
declare function getConfig(ctx: {
|
|
29
|
+
meta: Record<string, unknown>;
|
|
30
|
+
}, key: string, fallback?: unknown): unknown;
|
|
31
|
+
|
|
32
|
+
export { CONFIG_MANAGER_KEY as C, type IRemoteConfigOptions as I, RemoteConfigManager as R, configContextMiddleware as c, getConfig as g };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Middleware } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
+
|
|
4
|
+
interface IRemoteConfigOptions {
|
|
5
|
+
ttl?: number;
|
|
6
|
+
environment?: string;
|
|
7
|
+
table?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare class RemoteConfigManager {
|
|
11
|
+
private store;
|
|
12
|
+
private snapshot;
|
|
13
|
+
private interval;
|
|
14
|
+
private environment;
|
|
15
|
+
private ttl;
|
|
16
|
+
private table;
|
|
17
|
+
constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
|
|
18
|
+
boot(): Promise<void>;
|
|
19
|
+
stop(): void;
|
|
20
|
+
get<T>(key: string, fallback: T): T;
|
|
21
|
+
all(): Record<string, unknown>;
|
|
22
|
+
refresh(): Promise<void>;
|
|
23
|
+
isStale(): boolean;
|
|
24
|
+
}
|
|
25
|
+
declare const CONFIG_MANAGER_KEY = "fonderie.config.snapshot";
|
|
26
|
+
|
|
27
|
+
declare function configContextMiddleware(manager: RemoteConfigManager): Middleware;
|
|
28
|
+
declare function getConfig(ctx: {
|
|
29
|
+
meta: Record<string, unknown>;
|
|
30
|
+
}, key: string, fallback?: unknown): unknown;
|
|
31
|
+
|
|
32
|
+
export { CONFIG_MANAGER_KEY as C, type IRemoteConfigOptions as I, RemoteConfigManager as R, configContextMiddleware as c, getConfig as g };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CONFIG_MANAGER_KEY: () => CONFIG_MANAGER_KEY,
|
|
24
|
+
RemoteConfigManager: () => RemoteConfigManager,
|
|
25
|
+
RemoteConfigModule: () => RemoteConfigModule,
|
|
26
|
+
configContextMiddleware: () => configContextMiddleware,
|
|
27
|
+
deleteConfigEntry: () => deleteConfigEntry,
|
|
28
|
+
getConfig: () => getConfig,
|
|
29
|
+
getConfigEntry: () => getConfigEntry,
|
|
30
|
+
listConfigEntries: () => listConfigEntries,
|
|
31
|
+
setConfigEntry: () => setConfigEntry
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/manager.ts
|
|
36
|
+
var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
|
|
37
|
+
var RemoteConfigManager = class {
|
|
38
|
+
constructor(store, options = {}) {
|
|
39
|
+
this.store = store;
|
|
40
|
+
this.ttl = options.ttl ?? 3e4;
|
|
41
|
+
this.environment = options.environment ?? process.env["NODE_ENV"] ?? "development";
|
|
42
|
+
this.table = options.table ?? "fonderie_config";
|
|
43
|
+
}
|
|
44
|
+
store;
|
|
45
|
+
snapshot = null;
|
|
46
|
+
interval = null;
|
|
47
|
+
environment;
|
|
48
|
+
ttl;
|
|
49
|
+
table;
|
|
50
|
+
async boot() {
|
|
51
|
+
await this.refresh();
|
|
52
|
+
this.interval = setInterval(() => {
|
|
53
|
+
this.refresh().catch((err) => console.error("[config] refresh error:", err));
|
|
54
|
+
}, this.ttl);
|
|
55
|
+
}
|
|
56
|
+
stop() {
|
|
57
|
+
if (this.interval) {
|
|
58
|
+
clearInterval(this.interval);
|
|
59
|
+
this.interval = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Get a single value with a typed fallback
|
|
63
|
+
get(key, fallback) {
|
|
64
|
+
if (!this.snapshot) {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
const value = this.snapshot.entries[key];
|
|
68
|
+
return value !== void 0 ? value : fallback;
|
|
69
|
+
}
|
|
70
|
+
// Get all entries as a flat record
|
|
71
|
+
all() {
|
|
72
|
+
return this.snapshot?.entries ?? {};
|
|
73
|
+
}
|
|
74
|
+
async refresh() {
|
|
75
|
+
try {
|
|
76
|
+
const rows = await this.store.query(
|
|
77
|
+
`SELECT key, value, environment
|
|
78
|
+
FROM ${this.table}
|
|
79
|
+
WHERE (environment = $1 OR environment = 'all')
|
|
80
|
+
AND active = true`,
|
|
81
|
+
[this.environment]
|
|
82
|
+
);
|
|
83
|
+
const entries = {};
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
if (row.environment === "all") {
|
|
86
|
+
try {
|
|
87
|
+
entries[row.key] = JSON.parse(row.value);
|
|
88
|
+
} catch {
|
|
89
|
+
entries[row.key] = row.value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
if (row.environment !== "all") {
|
|
95
|
+
try {
|
|
96
|
+
entries[row.key] = JSON.parse(row.value);
|
|
97
|
+
} catch {
|
|
98
|
+
entries[row.key] = row.value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.snapshot = {
|
|
103
|
+
entries,
|
|
104
|
+
fetchedAt: /* @__PURE__ */ new Date()
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("[config] failed to refresh:", err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
isStale() {
|
|
111
|
+
if (!this.snapshot) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
|
|
118
|
+
|
|
119
|
+
// src/middlewares/config-context.ts
|
|
120
|
+
function configContextMiddleware(manager) {
|
|
121
|
+
return async (ctx, next) => {
|
|
122
|
+
ctx.meta[CONFIG_MANAGER_KEY] = manager;
|
|
123
|
+
return next();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function getConfig(ctx, key, fallback = null) {
|
|
127
|
+
const manager = ctx.meta[CONFIG_MANAGER_KEY];
|
|
128
|
+
if (!manager) {
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
return manager.get(key, fallback);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/module.ts
|
|
135
|
+
var RemoteConfigModule = class {
|
|
136
|
+
name = "@fonderie/config";
|
|
137
|
+
manager;
|
|
138
|
+
constructor(store, options = {}) {
|
|
139
|
+
this.manager = new RemoteConfigManager(store, options);
|
|
140
|
+
}
|
|
141
|
+
async install(app) {
|
|
142
|
+
await this.manager.boot();
|
|
143
|
+
app.use(configContextMiddleware(this.manager));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/services/config.ts
|
|
148
|
+
var SELECT_ENTRY = `
|
|
149
|
+
SELECT
|
|
150
|
+
key,
|
|
151
|
+
value,
|
|
152
|
+
environment,
|
|
153
|
+
description,
|
|
154
|
+
active,
|
|
155
|
+
updated_at AS "updatedAt"
|
|
156
|
+
FROM fonderie_config`;
|
|
157
|
+
async function listConfigEntries(environment, store) {
|
|
158
|
+
return environment ? store.query(
|
|
159
|
+
`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
|
|
160
|
+
[environment]
|
|
161
|
+
) : store.query(`${SELECT_ENTRY} ORDER BY environment, key`);
|
|
162
|
+
}
|
|
163
|
+
async function getConfigEntry(key, environment, store) {
|
|
164
|
+
const [row] = await store.query(
|
|
165
|
+
`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,
|
|
166
|
+
[key, environment]
|
|
167
|
+
);
|
|
168
|
+
return row ?? null;
|
|
169
|
+
}
|
|
170
|
+
async function setConfigEntry(opts, store) {
|
|
171
|
+
const env = opts.environment ?? "all";
|
|
172
|
+
const raw = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
|
|
173
|
+
const active = opts.active ?? true;
|
|
174
|
+
const [row] = await store.query(
|
|
175
|
+
`INSERT INTO fonderie_config (key, value, environment, description, active)
|
|
176
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
177
|
+
ON CONFLICT (key, environment) DO UPDATE SET
|
|
178
|
+
value = $2,
|
|
179
|
+
description = COALESCE($4, fonderie_config.description),
|
|
180
|
+
active = $5,
|
|
181
|
+
updated_at = now()
|
|
182
|
+
RETURNING
|
|
183
|
+
key, value, environment, description, active,
|
|
184
|
+
updated_at AS "updatedAt"`,
|
|
185
|
+
[opts.key, raw, env, opts.description ?? null, active]
|
|
186
|
+
);
|
|
187
|
+
if (!row) throw new Error("Failed to upsert config entry");
|
|
188
|
+
return row;
|
|
189
|
+
}
|
|
190
|
+
async function deleteConfigEntry(key, environment, store) {
|
|
191
|
+
const rows = await store.query(
|
|
192
|
+
`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,
|
|
193
|
+
[key, environment]
|
|
194
|
+
);
|
|
195
|
+
return rows.length > 0;
|
|
196
|
+
}
|
|
197
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
198
|
+
0 && (module.exports = {
|
|
199
|
+
CONFIG_MANAGER_KEY,
|
|
200
|
+
RemoteConfigManager,
|
|
201
|
+
RemoteConfigModule,
|
|
202
|
+
configContextMiddleware,
|
|
203
|
+
deleteConfigEntry,
|
|
204
|
+
getConfig,
|
|
205
|
+
getConfigEntry,
|
|
206
|
+
listConfigEntries,
|
|
207
|
+
setConfigEntry
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/manager.ts","../src/middlewares/config-context.ts","../src/module.ts","../src/services/config.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { RemoteConfigModule } from './module';\nexport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nexport { configContextMiddleware, getConfig } from './middlewares/config-context';\nexport {\n\tlistConfigEntries,\n\tgetConfigEntry,\n\tsetConfigEntry,\n\tdeleteConfigEntry,\n} from './services/config';\n\nexport type { IConfigEntry, IConfigSnapshot } from './types';\nexport type { IRemoteConfigOptions } from './config';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IRemoteConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\n\nimport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nimport { configContextMiddleware } from './middlewares/config-context';\n\nexport class RemoteConfigModule implements IFonderieModule {\n\treadonly name = '@fonderie/config';\n\treadonly manager: RemoteConfigManager;\n\n\tconstructor(store: IStoreAdapter, options: IRemoteConfigOptions = {}) {\n\t\tthis.manager = new RemoteConfigManager(store, options);\n\t}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait this.manager.boot();\n\t\tapp.use(configContextMiddleware(this.manager));\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigEntry } from '../types';\n\nconst SELECT_ENTRY = `\n\tSELECT\n\t\tkey,\n\t\tvalue,\n\t\tenvironment,\n\t\tdescription,\n\t\tactive,\n\t\tupdated_at AS \"updatedAt\"\n\tFROM fonderie_config`;\n\nexport async function listConfigEntries(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry[]> {\n\treturn environment\n\t\t? store.query<IConfigEntry>(\n\t\t\t\t`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<IConfigEntry>(`${SELECT_ENTRY} ORDER BY environment, key`);\n}\n\nexport async function getConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry | null> {\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\nexport async function setConfigEntry(\n\topts: {\n\t\tkey: string;\n\t\tvalue: unknown;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\tconst env = opts.environment ?? 'all';\n\tconst raw = typeof opts.value === 'string' ? opts.value : JSON.stringify(opts.value);\n\tconst active = opts.active ?? true;\n\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`INSERT INTO fonderie_config (key, value, environment, description, active)\n\t\tVALUES ($1, $2, $3, $4, $5)\n\t\tON CONFLICT (key, environment) DO UPDATE SET\n\t\t\tvalue = $2,\n\t\t\tdescription = COALESCE($4, fonderie_config.description),\n\t\t\tactive = $5,\n\t\t\tupdated_at = now()\n\t\tRETURNING\n\t\t\tkey, value, environment, description, active,\n\t\t\tupdated_at AS \"updatedAt\"`,\n\t\t[opts.key, raw, env, opts.description ?? null, active],\n\t);\n\tif (!row) throw new Error('Failed to upsert config entry');\n\treturn row;\n}\n\nexport async function deleteConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
+
import { R as RemoteConfigManager, I as IRemoteConfigOptions } from './index-v2hLmm2m.cjs';
|
|
4
|
+
export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, g as getConfig } from './index-v2hLmm2m.cjs';
|
|
5
|
+
import { IConfigEntry } from './types.cjs';
|
|
6
|
+
export { IConfigSnapshot } from './types.cjs';
|
|
7
|
+
|
|
8
|
+
declare class RemoteConfigModule implements IFonderieModule {
|
|
9
|
+
readonly name = "@fonderie/config";
|
|
10
|
+
readonly manager: RemoteConfigManager;
|
|
11
|
+
constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
|
|
12
|
+
install(app: IFonderieApp): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare function listConfigEntries(environment: string | null, store: IStoreAdapter): Promise<IConfigEntry[]>;
|
|
16
|
+
declare function getConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<IConfigEntry | null>;
|
|
17
|
+
declare function setConfigEntry(opts: {
|
|
18
|
+
key: string;
|
|
19
|
+
value: unknown;
|
|
20
|
+
environment?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
active?: boolean;
|
|
23
|
+
}, store: IStoreAdapter): Promise<IConfigEntry>;
|
|
24
|
+
declare function deleteConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
|
|
25
|
+
|
|
26
|
+
export { IConfigEntry, IRemoteConfigOptions, RemoteConfigManager, RemoteConfigModule, deleteConfigEntry, getConfigEntry, listConfigEntries, setConfigEntry };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
+
import { R as RemoteConfigManager, I as IRemoteConfigOptions } from './index-v2hLmm2m.js';
|
|
4
|
+
export { C as CONFIG_MANAGER_KEY, c as configContextMiddleware, g as getConfig } from './index-v2hLmm2m.js';
|
|
5
|
+
import { IConfigEntry } from './types.js';
|
|
6
|
+
export { IConfigSnapshot } from './types.js';
|
|
7
|
+
|
|
8
|
+
declare class RemoteConfigModule implements IFonderieModule {
|
|
9
|
+
readonly name = "@fonderie/config";
|
|
10
|
+
readonly manager: RemoteConfigManager;
|
|
11
|
+
constructor(store: IStoreAdapter, options?: IRemoteConfigOptions);
|
|
12
|
+
install(app: IFonderieApp): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare function listConfigEntries(environment: string | null, store: IStoreAdapter): Promise<IConfigEntry[]>;
|
|
16
|
+
declare function getConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<IConfigEntry | null>;
|
|
17
|
+
declare function setConfigEntry(opts: {
|
|
18
|
+
key: string;
|
|
19
|
+
value: unknown;
|
|
20
|
+
environment?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
active?: boolean;
|
|
23
|
+
}, store: IStoreAdapter): Promise<IConfigEntry>;
|
|
24
|
+
declare function deleteConfigEntry(key: string, environment: string, store: IStoreAdapter): Promise<boolean>;
|
|
25
|
+
|
|
26
|
+
export { IConfigEntry, IRemoteConfigOptions, RemoteConfigManager, RemoteConfigModule, deleteConfigEntry, getConfigEntry, listConfigEntries, setConfigEntry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/manager.ts
|
|
2
|
+
var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
|
|
3
|
+
var RemoteConfigManager = class {
|
|
4
|
+
constructor(store, options = {}) {
|
|
5
|
+
this.store = store;
|
|
6
|
+
this.ttl = options.ttl ?? 3e4;
|
|
7
|
+
this.environment = options.environment ?? process.env["NODE_ENV"] ?? "development";
|
|
8
|
+
this.table = options.table ?? "fonderie_config";
|
|
9
|
+
}
|
|
10
|
+
store;
|
|
11
|
+
snapshot = null;
|
|
12
|
+
interval = null;
|
|
13
|
+
environment;
|
|
14
|
+
ttl;
|
|
15
|
+
table;
|
|
16
|
+
async boot() {
|
|
17
|
+
await this.refresh();
|
|
18
|
+
this.interval = setInterval(() => {
|
|
19
|
+
this.refresh().catch((err) => console.error("[config] refresh error:", err));
|
|
20
|
+
}, this.ttl);
|
|
21
|
+
}
|
|
22
|
+
stop() {
|
|
23
|
+
if (this.interval) {
|
|
24
|
+
clearInterval(this.interval);
|
|
25
|
+
this.interval = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Get a single value with a typed fallback
|
|
29
|
+
get(key, fallback) {
|
|
30
|
+
if (!this.snapshot) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const value = this.snapshot.entries[key];
|
|
34
|
+
return value !== void 0 ? value : fallback;
|
|
35
|
+
}
|
|
36
|
+
// Get all entries as a flat record
|
|
37
|
+
all() {
|
|
38
|
+
return this.snapshot?.entries ?? {};
|
|
39
|
+
}
|
|
40
|
+
async refresh() {
|
|
41
|
+
try {
|
|
42
|
+
const rows = await this.store.query(
|
|
43
|
+
`SELECT key, value, environment
|
|
44
|
+
FROM ${this.table}
|
|
45
|
+
WHERE (environment = $1 OR environment = 'all')
|
|
46
|
+
AND active = true`,
|
|
47
|
+
[this.environment]
|
|
48
|
+
);
|
|
49
|
+
const entries = {};
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (row.environment === "all") {
|
|
52
|
+
try {
|
|
53
|
+
entries[row.key] = JSON.parse(row.value);
|
|
54
|
+
} catch {
|
|
55
|
+
entries[row.key] = row.value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
if (row.environment !== "all") {
|
|
61
|
+
try {
|
|
62
|
+
entries[row.key] = JSON.parse(row.value);
|
|
63
|
+
} catch {
|
|
64
|
+
entries[row.key] = row.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.snapshot = {
|
|
69
|
+
entries,
|
|
70
|
+
fetchedAt: /* @__PURE__ */ new Date()
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error("[config] failed to refresh:", err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
isStale() {
|
|
77
|
+
if (!this.snapshot) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
|
|
84
|
+
|
|
85
|
+
// src/middlewares/config-context.ts
|
|
86
|
+
function configContextMiddleware(manager) {
|
|
87
|
+
return async (ctx, next) => {
|
|
88
|
+
ctx.meta[CONFIG_MANAGER_KEY] = manager;
|
|
89
|
+
return next();
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function getConfig(ctx, key, fallback = null) {
|
|
93
|
+
const manager = ctx.meta[CONFIG_MANAGER_KEY];
|
|
94
|
+
if (!manager) {
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
return manager.get(key, fallback);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/module.ts
|
|
101
|
+
var RemoteConfigModule = class {
|
|
102
|
+
name = "@fonderie/config";
|
|
103
|
+
manager;
|
|
104
|
+
constructor(store, options = {}) {
|
|
105
|
+
this.manager = new RemoteConfigManager(store, options);
|
|
106
|
+
}
|
|
107
|
+
async install(app) {
|
|
108
|
+
await this.manager.boot();
|
|
109
|
+
app.use(configContextMiddleware(this.manager));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/services/config.ts
|
|
114
|
+
var SELECT_ENTRY = `
|
|
115
|
+
SELECT
|
|
116
|
+
key,
|
|
117
|
+
value,
|
|
118
|
+
environment,
|
|
119
|
+
description,
|
|
120
|
+
active,
|
|
121
|
+
updated_at AS "updatedAt"
|
|
122
|
+
FROM fonderie_config`;
|
|
123
|
+
async function listConfigEntries(environment, store) {
|
|
124
|
+
return environment ? store.query(
|
|
125
|
+
`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,
|
|
126
|
+
[environment]
|
|
127
|
+
) : store.query(`${SELECT_ENTRY} ORDER BY environment, key`);
|
|
128
|
+
}
|
|
129
|
+
async function getConfigEntry(key, environment, store) {
|
|
130
|
+
const [row] = await store.query(
|
|
131
|
+
`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,
|
|
132
|
+
[key, environment]
|
|
133
|
+
);
|
|
134
|
+
return row ?? null;
|
|
135
|
+
}
|
|
136
|
+
async function setConfigEntry(opts, store) {
|
|
137
|
+
const env = opts.environment ?? "all";
|
|
138
|
+
const raw = typeof opts.value === "string" ? opts.value : JSON.stringify(opts.value);
|
|
139
|
+
const active = opts.active ?? true;
|
|
140
|
+
const [row] = await store.query(
|
|
141
|
+
`INSERT INTO fonderie_config (key, value, environment, description, active)
|
|
142
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
143
|
+
ON CONFLICT (key, environment) DO UPDATE SET
|
|
144
|
+
value = $2,
|
|
145
|
+
description = COALESCE($4, fonderie_config.description),
|
|
146
|
+
active = $5,
|
|
147
|
+
updated_at = now()
|
|
148
|
+
RETURNING
|
|
149
|
+
key, value, environment, description, active,
|
|
150
|
+
updated_at AS "updatedAt"`,
|
|
151
|
+
[opts.key, raw, env, opts.description ?? null, active]
|
|
152
|
+
);
|
|
153
|
+
if (!row) throw new Error("Failed to upsert config entry");
|
|
154
|
+
return row;
|
|
155
|
+
}
|
|
156
|
+
async function deleteConfigEntry(key, environment, store) {
|
|
157
|
+
const rows = await store.query(
|
|
158
|
+
`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,
|
|
159
|
+
[key, environment]
|
|
160
|
+
);
|
|
161
|
+
return rows.length > 0;
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
CONFIG_MANAGER_KEY,
|
|
165
|
+
RemoteConfigManager,
|
|
166
|
+
RemoteConfigModule,
|
|
167
|
+
configContextMiddleware,
|
|
168
|
+
deleteConfigEntry,
|
|
169
|
+
getConfig,
|
|
170
|
+
getConfigEntry,
|
|
171
|
+
listConfigEntries,
|
|
172
|
+
setConfigEntry
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/manager.ts","../src/middlewares/config-context.ts","../src/module.ts","../src/services/config.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IRemoteConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\n\nimport { RemoteConfigManager, CONFIG_MANAGER_KEY } from './manager';\nimport { configContextMiddleware } from './middlewares/config-context';\n\nexport class RemoteConfigModule implements IFonderieModule {\n\treadonly name = '@fonderie/config';\n\treadonly manager: RemoteConfigManager;\n\n\tconstructor(store: IStoreAdapter, options: IRemoteConfigOptions = {}) {\n\t\tthis.manager = new RemoteConfigManager(store, options);\n\t}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait this.manager.boot();\n\t\tapp.use(configContextMiddleware(this.manager));\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IConfigEntry } from '../types';\n\nconst SELECT_ENTRY = `\n\tSELECT\n\t\tkey,\n\t\tvalue,\n\t\tenvironment,\n\t\tdescription,\n\t\tactive,\n\t\tupdated_at AS \"updatedAt\"\n\tFROM fonderie_config`;\n\nexport async function listConfigEntries(\n\tenvironment: string | null,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry[]> {\n\treturn environment\n\t\t? store.query<IConfigEntry>(\n\t\t\t\t`${SELECT_ENTRY} WHERE (environment = $1 OR environment = 'all') AND active = true ORDER BY key`,\n\t\t\t\t[environment],\n\t\t\t)\n\t\t: store.query<IConfigEntry>(`${SELECT_ENTRY} ORDER BY environment, key`);\n}\n\nexport async function getConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry | null> {\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`${SELECT_ENTRY} WHERE key = $1 AND environment = $2`,\n\t\t[key, environment],\n\t);\n\treturn row ?? null;\n}\n\nexport async function setConfigEntry(\n\topts: {\n\t\tkey: string;\n\t\tvalue: unknown;\n\t\tenvironment?: string;\n\t\tdescription?: string;\n\t\tactive?: boolean;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IConfigEntry> {\n\tconst env = opts.environment ?? 'all';\n\tconst raw = typeof opts.value === 'string' ? opts.value : JSON.stringify(opts.value);\n\tconst active = opts.active ?? true;\n\n\tconst [row] = await store.query<IConfigEntry>(\n\t\t`INSERT INTO fonderie_config (key, value, environment, description, active)\n\t\tVALUES ($1, $2, $3, $4, $5)\n\t\tON CONFLICT (key, environment) DO UPDATE SET\n\t\t\tvalue = $2,\n\t\t\tdescription = COALESCE($4, fonderie_config.description),\n\t\t\tactive = $5,\n\t\t\tupdated_at = now()\n\t\tRETURNING\n\t\t\tkey, value, environment, description, active,\n\t\t\tupdated_at AS \"updatedAt\"`,\n\t\t[opts.key, raw, env, opts.description ?? null, active],\n\t);\n\tif (!row) throw new Error('Failed to upsert config entry');\n\treturn row;\n}\n\nexport async function deleteConfigEntry(\n\tkey: string,\n\tenvironment: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ key: string }>(\n\t\t`DELETE FROM fonderie_config WHERE key = $1 AND environment = $2 RETURNING key`,\n\t\t[key, environment],\n\t);\n\treturn rows.length > 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middlewares/index.ts
|
|
21
|
+
var middlewares_exports = {};
|
|
22
|
+
__export(middlewares_exports, {
|
|
23
|
+
configContextMiddleware: () => configContextMiddleware,
|
|
24
|
+
getConfig: () => getConfig
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(middlewares_exports);
|
|
27
|
+
|
|
28
|
+
// src/manager.ts
|
|
29
|
+
var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
|
|
30
|
+
var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
|
|
31
|
+
|
|
32
|
+
// src/middlewares/config-context.ts
|
|
33
|
+
function configContextMiddleware(manager) {
|
|
34
|
+
return async (ctx, next) => {
|
|
35
|
+
ctx.meta[CONFIG_MANAGER_KEY] = manager;
|
|
36
|
+
return next();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function getConfig(ctx, key, fallback = null) {
|
|
40
|
+
const manager = ctx.meta[CONFIG_MANAGER_KEY];
|
|
41
|
+
if (!manager) {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
return manager.get(key, fallback);
|
|
45
|
+
}
|
|
46
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
47
|
+
0 && (module.exports = {
|
|
48
|
+
configContextMiddleware,
|
|
49
|
+
getConfig
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/manager.ts","../../src/middlewares/config-context.ts"],"sourcesContent":["export { configContextMiddleware, getConfig } from './config-context';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IRemoteConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,sBAAsB;AAwGrB,IAAM,qBAAqB;;;ACpG3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/manager.ts
|
|
2
|
+
var FONDERIE_CONFIG_KEY = "fonderie.config.snapshot";
|
|
3
|
+
var CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;
|
|
4
|
+
|
|
5
|
+
// src/middlewares/config-context.ts
|
|
6
|
+
function configContextMiddleware(manager) {
|
|
7
|
+
return async (ctx, next) => {
|
|
8
|
+
ctx.meta[CONFIG_MANAGER_KEY] = manager;
|
|
9
|
+
return next();
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function getConfig(ctx, key, fallback = null) {
|
|
13
|
+
const manager = ctx.meta[CONFIG_MANAGER_KEY];
|
|
14
|
+
if (!manager) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return manager.get(key, fallback);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
configContextMiddleware,
|
|
21
|
+
getConfig
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/manager.ts","../../src/middlewares/config-context.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRemoteConfigOptions } from './config';\nimport type { IConfigSnapshot } from './types';\n\nconst FONDERIE_CONFIG_KEY = 'fonderie.config.snapshot';\n\nexport class RemoteConfigManager {\n\tprivate snapshot: IConfigSnapshot | null = null;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\tprivate environment: string;\n\tprivate ttl: number;\n\tprivate table: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\toptions: IRemoteConfigOptions = {},\n\t) {\n\t\tthis.ttl = options.ttl ?? 30_000;\n\t\tthis.environment = options.environment ?? process.env['NODE_ENV'] ?? 'development';\n\t\tthis.table = options.table ?? 'fonderie_config';\n\t}\n\n\tasync boot(): Promise<void> {\n\t\tawait this.refresh();\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.refresh().catch((err) => console.error('[config] refresh error:', err));\n\t\t}, this.ttl);\n\t}\n\n\tstop(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\t// Get a single value with a typed fallback\n\tget<T>(key: string, fallback: T): T {\n\t\tif (!this.snapshot) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst value = this.snapshot.entries[key];\n\t\treturn value !== undefined ? (value as T) : fallback;\n\t}\n\n\t// Get all entries as a flat record\n\tall(): Record<string, unknown> {\n\t\treturn this.snapshot?.entries ?? {};\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\ttry {\n\t\t\tconst rows = await this.store.query<{\n\t\t\t\tkey: string;\n\t\t\t\tvalue: string;\n\t\t\t\tenvironment: string;\n\t\t\t}>(\n\t\t\t\t`SELECT key, value, environment\n\t\t\t\t FROM ${this.table}\n\t\t\t\t WHERE (environment = $1 OR environment = 'all')\n\t\t\t\t\tAND active = true`,\n\t\t\t\t[this.environment],\n\t\t\t);\n\n\t\t\tconst entries: Record<string, unknown> = {};\n\n\t\t\t// First pass — load 'all' entries as base\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment === 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Second pass — environment-specific entries override 'all'\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.environment !== 'all') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries[row.key] = JSON.parse(row.value);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tentries[row.key] = row.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.snapshot = {\n\t\t\t\tentries,\n\t\t\t\tfetchedAt: new Date(),\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconsole.error('[config] failed to refresh:', err);\n\t\t}\n\t}\n\n\tisStale(): boolean {\n\t\tif (!this.snapshot) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn Date.now() - this.snapshot.fetchedAt.getTime() > this.ttl;\n\t}\n}\n\n// Well-known config keys — extend as needed\nexport const CONFIG_MANAGER_KEY = FONDERIE_CONFIG_KEY;\n","import type { Middleware } from '@fonderie/core';\n\nimport type { RemoteConfigManager } from '../manager';\n\nimport { CONFIG_MANAGER_KEY } from '../manager';\n\n// Injects the config manager into ctx.meta\n// so handlers can read config values per-request\n\nexport function configContextMiddleware(manager: RemoteConfigManager): Middleware {\n\treturn async (ctx, next) => {\n\t\tctx.meta[CONFIG_MANAGER_KEY] = manager;\n\t\treturn next();\n\t};\n}\n\n// Helper to read config from ctx inside a handler\nexport function getConfig(\n\tctx: { meta: Record<string, unknown> },\n\tkey: string,\n\tfallback: unknown = null,\n): unknown {\n\tconst manager = ctx.meta[CONFIG_MANAGER_KEY] as RemoteConfigManager | undefined;\n\tif (!manager) {\n\t\treturn fallback;\n\t}\n\n\treturn manager.get(key, fallback);\n}\n"],"mappings":";AAKA,IAAM,sBAAsB;AAwGrB,IAAM,qBAAqB;;;ACpG3B,SAAS,wBAAwB,SAA0C;AACjF,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,KAAK,kBAAkB,IAAI;AAC/B,WAAO,KAAK;AAAA,EACb;AACD;AAGO,SAAS,UACf,KACA,KACA,WAAoB,MACV;AACV,QAAM,UAAU,IAAI,KAAK,kBAAkB;AAC3C,MAAI,CAAC,SAAS;AACb,WAAO;AAAA,EACR;AAEA,SAAO,QAAQ,IAAI,KAAK,QAAQ;AACjC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS fonderie_config (
|
|
2
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
key TEXT NOT NULL,
|
|
4
|
+
value TEXT NOT NULL,
|
|
5
|
+
environment TEXT NOT NULL DEFAULT 'all',
|
|
6
|
+
description TEXT,
|
|
7
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
8
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
9
|
+
UNIQUE (key, environment)
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS fonderie_config_env_active_idx
|
|
13
|
+
ON fonderie_config (environment, active);
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
19
|
+
//# sourceMappingURL=types.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface IConfigEntry {\n\tkey: string;\n\tvalue: unknown;\n\tenvironment: string;\n\tdescription: string | null;\n\tactive: boolean;\n\tupdatedAt: string;\n}\n\nexport interface IConfigSnapshot {\n\tentries: Record<string, unknown>;\n\tfetchedAt: Date;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface IConfigEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
value: unknown;
|
|
4
|
+
environment: string;
|
|
5
|
+
description: string | null;
|
|
6
|
+
active: boolean;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
interface IConfigSnapshot {
|
|
10
|
+
entries: Record<string, unknown>;
|
|
11
|
+
fetchedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type { IConfigEntry, IConfigSnapshot };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface IConfigEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
value: unknown;
|
|
4
|
+
environment: string;
|
|
5
|
+
description: string | null;
|
|
6
|
+
active: boolean;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
interface IConfigSnapshot {
|
|
10
|
+
entries: Record<string, unknown>;
|
|
11
|
+
fetchedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type { IConfigEntry, IConfigSnapshot };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fonderie/config",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DB-backed feature flags and remote config — per-environment overrides, TTL-based hot reload, and a typed getConfig helper usable inside any middleware.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fonderie-js",
|
|
7
|
+
"feature-flags",
|
|
8
|
+
"remote-config",
|
|
9
|
+
"runtime-config",
|
|
10
|
+
"saas",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./dist/types.d.ts",
|
|
26
|
+
"import": "./dist/types.js",
|
|
27
|
+
"require": "./dist/types.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./middleware": {
|
|
30
|
+
"types": "./dist/middlewares/index.d.ts",
|
|
31
|
+
"import": "./dist/middlewares/index.js",
|
|
32
|
+
"require": "./dist/middlewares/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./migrations": {
|
|
35
|
+
"types": "./dist/migrations/index.d.ts",
|
|
36
|
+
"import": "./dist/migrations/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.cjs",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup && node -e \"require('node:fs').cpSync('src/migrations/sql', 'dist/migrations/sql', {recursive:true})\"",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
47
|
+
"lint": "biome lint src",
|
|
48
|
+
"format": "biome format --write src",
|
|
49
|
+
"check": "biome check --write src"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@fonderie/core": "^0.1.0",
|
|
53
|
+
"@fonderie/store": "^0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@fonderie/core": "../core",
|
|
57
|
+
"@fonderie/store": "../store",
|
|
58
|
+
"@types/node": "^25.6.0",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^6.0.3"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"dist",
|
|
68
|
+
"LICENSE",
|
|
69
|
+
"README.md"
|
|
70
|
+
],
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "git+https://github.com/fonderie-js/sdk.git",
|
|
74
|
+
"directory": "packages/config"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/config#readme",
|
|
77
|
+
"bugs": {
|
|
78
|
+
"url": "https://github.com/fonderie-js/sdk/issues"
|
|
79
|
+
}
|
|
80
|
+
}
|