@effected/config-file 0.13.1 → 0.14.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/ConfigEvent.js +6 -7
- package/ConfigFile.js +50 -30
- package/ConfigMigration.js +18 -2
- package/ConfigProvider.js +13 -9
- package/EncryptedCodec.js +11 -4
- package/MergeStrategy.js +11 -3
- package/README.md +4 -4
- package/index.d.ts +119 -54
- package/package.json +7 -6
package/ConfigEvent.js
CHANGED
|
@@ -21,15 +21,14 @@ const ConfigSourceRef = Schema.Struct({
|
|
|
21
21
|
* persistence.
|
|
22
22
|
*
|
|
23
23
|
* @remarks
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* never observe a discovery failure to report one.
|
|
24
|
+
* There is no discovery-failure variant: under this package's
|
|
25
|
+
* resolver-absorption contract a resolver's error channel is `never` — every
|
|
26
|
+
* filesystem failure becomes `Option.none()` — so the pipeline never observes a
|
|
27
|
+
* discovery failure to report.
|
|
29
28
|
*
|
|
30
29
|
* The failure variants carry the **structured** typed error in `error`, not a
|
|
31
|
-
* `reason` string
|
|
32
|
-
*
|
|
30
|
+
* `reason` string, so every field a subscriber might branch on survives; a
|
|
31
|
+
* subscriber that wants prose can read `error.message`.
|
|
33
32
|
*
|
|
34
33
|
* @public
|
|
35
34
|
*/
|
package/ConfigFile.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Context, DateTime, Effect, FileSystem, Layer, Option, Path, PubSub, Sch
|
|
|
10
10
|
* @remarks
|
|
11
11
|
* Its own tag, so "no config anywhere" is routable with `Effect.catchTag`
|
|
12
12
|
* separately from "the config I found is broken" — the single most important
|
|
13
|
-
* distinction the
|
|
13
|
+
* distinction in the error set.
|
|
14
14
|
*
|
|
15
15
|
* @public
|
|
16
16
|
*/
|
|
@@ -40,8 +40,8 @@ var ConfigFileNotFoundError = class extends Schema.TaggedError()("ConfigFileNotF
|
|
|
40
40
|
* Indicates that a config file could not be read from the filesystem.
|
|
41
41
|
*
|
|
42
42
|
* @remarks
|
|
43
|
-
* `cause` preserves the underlying filesystem failure structurally
|
|
44
|
-
* it to
|
|
43
|
+
* `cause` preserves the underlying filesystem failure structurally rather than
|
|
44
|
+
* flattening it to a string.
|
|
45
45
|
*
|
|
46
46
|
* @public
|
|
47
47
|
*/
|
|
@@ -75,13 +75,12 @@ var ConfigFileWriteError = class extends Schema.TaggedError()("ConfigFileWriteEr
|
|
|
75
75
|
* was called on a service configured without a `defaultPath`.
|
|
76
76
|
*
|
|
77
77
|
* @remarks
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* write failure.
|
|
78
|
+
* It is distinct by tag from a real {@link ConfigFileWriteError}, so a caller
|
|
79
|
+
* can tell "no destination was configured" from "the write failed".
|
|
81
80
|
*
|
|
82
81
|
* It carries no `path` field on purpose. The whole point of this failure is
|
|
83
82
|
* that there is no path; a {@link ConfigFileWriteError} with a fabricated path
|
|
84
|
-
* would be a lie
|
|
83
|
+
* would be a lie.
|
|
85
84
|
*
|
|
86
85
|
* @public
|
|
87
86
|
*/
|
|
@@ -96,10 +95,9 @@ var ConfigDefaultPathMissingError = class extends Schema.TaggedError()("ConfigDe
|
|
|
96
95
|
*
|
|
97
96
|
* @remarks
|
|
98
97
|
* `issue` carries the **structured** schema failure — at runtime a
|
|
99
|
-
* `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* narrow it with the `SchemaIssue` module.
|
|
98
|
+
* `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`, so
|
|
99
|
+
* every field a caller might branch on survives. It is typed `unknown` because
|
|
100
|
+
* there is no `Schema` for `Issue`; narrow it with the `SchemaIssue` module.
|
|
103
101
|
*
|
|
104
102
|
* @public
|
|
105
103
|
*/
|
|
@@ -244,10 +242,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
244
242
|
const searched = options.resolvers.map((r) => r.name);
|
|
245
243
|
/**
|
|
246
244
|
* Merge the discovered sources and announce the result. Shared by `load` and
|
|
247
|
-
* `loadOrDefault`,
|
|
245
|
+
* `loadOrDefault`, so the two cannot drift apart.
|
|
248
246
|
*
|
|
249
|
-
* Both events carry EVERY contributing source
|
|
250
|
-
*
|
|
247
|
+
* Both events carry EVERY contributing source, because under `layeredMerge`
|
|
248
|
+
* all of them contributed.
|
|
251
249
|
*/
|
|
252
250
|
const mergeAndEmit = (sources) => Effect.gen(function* () {
|
|
253
251
|
const value = yield* options.strategy.resolve(sources);
|
|
@@ -331,10 +329,10 @@ const makeImpl = (options, fs, path, resolverEnv) => {
|
|
|
331
329
|
* Resolve `defaultPath`, `mkdir -p` its parent and write. Shared by `save`
|
|
332
330
|
* and `update`, and — crucially — emits nothing.
|
|
333
331
|
*
|
|
334
|
-
*
|
|
335
|
-
* `
|
|
336
|
-
*
|
|
337
|
-
*
|
|
332
|
+
* `update` must not call the public `save`, or one `update` would publish
|
|
333
|
+
* `Saved` as well as `Updated`. Event granularity is per-operation, so the
|
|
334
|
+
* emitting boundary sits in the public method, never in the shared internals
|
|
335
|
+
* it delegates to.
|
|
338
336
|
*/
|
|
339
337
|
const saveTo = (value) => Effect.gen(function* () {
|
|
340
338
|
const configured = options.defaultPath;
|
|
@@ -459,14 +457,30 @@ var ConfigFile = class {
|
|
|
459
457
|
* Build the live layer for a config service class.
|
|
460
458
|
*
|
|
461
459
|
* @remarks
|
|
462
|
-
* Resolver requirements flow into the layer's `R` type
|
|
463
|
-
* `
|
|
464
|
-
*
|
|
460
|
+
* Resolver requirements flow into the layer's `R` type: the result is
|
|
461
|
+
* `Layer<Self, never, FileSystem | Path | RR>`, so the platform services and
|
|
462
|
+
* every resolver's needs are visible at the provide site.
|
|
465
463
|
*
|
|
466
464
|
* `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
|
|
467
465
|
* twice builds two independent service instances. Bind its result to a const
|
|
468
|
-
* and provide that const
|
|
469
|
-
*
|
|
466
|
+
* and provide that const — do not call `ConfigFile.layer(...)` inline at each
|
|
467
|
+
* provide site.
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```ts
|
|
471
|
+
* import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
|
|
472
|
+
* import { Schema } from "effect";
|
|
473
|
+
*
|
|
474
|
+
* const AppShape = Schema.Struct({ port: Schema.Number });
|
|
475
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
|
|
476
|
+
*
|
|
477
|
+
* const AppConfigLive = ConfigFile.layer(AppConfig, {
|
|
478
|
+
* schema: AppShape,
|
|
479
|
+
* codec: JsonCodec,
|
|
480
|
+
* resolvers: [ConfigResolver.explicitPath("./app.config.json")],
|
|
481
|
+
* strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
|
|
482
|
+
* });
|
|
483
|
+
* ```
|
|
470
484
|
*/
|
|
471
485
|
static layer = layer;
|
|
472
486
|
/**
|
|
@@ -485,10 +499,9 @@ var ConfigFile = class {
|
|
|
485
499
|
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
486
500
|
* `node:fs`.
|
|
487
501
|
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* layer's requirements.
|
|
502
|
+
* The temp directory is removed by a finalizer bound to the layer's own scope,
|
|
503
|
+
* so cleanup runs on release without surfacing `Scope` in the layer's
|
|
504
|
+
* requirements.
|
|
492
505
|
*
|
|
493
506
|
* @example
|
|
494
507
|
* ```ts
|
|
@@ -524,10 +537,17 @@ var ConfigFile = class {
|
|
|
524
537
|
* @example
|
|
525
538
|
* ```ts
|
|
526
539
|
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
540
|
+
* import { Effect, Schema } from "effect";
|
|
541
|
+
*
|
|
542
|
+
* const MyConfig = Schema.Struct({ port: Schema.Number });
|
|
527
543
|
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
544
|
+
* // Requires `FileSystem` in `R`; provide it from a platform layer.
|
|
545
|
+
* const program = Effect.gen(function* () {
|
|
546
|
+
* const config = yield* ConfigFile.read("./app.config.json", {
|
|
547
|
+
* schema: MyConfig,
|
|
548
|
+
* codec: JsonCodec,
|
|
549
|
+
* });
|
|
550
|
+
* return config.port;
|
|
531
551
|
* });
|
|
532
552
|
* ```
|
|
533
553
|
*/
|
package/ConfigMigration.js
CHANGED
|
@@ -7,8 +7,7 @@ import { Effect, Schema } from "effect";
|
|
|
7
7
|
* @remarks
|
|
8
8
|
* `phase` says where: reading the current version, applying a step, or writing
|
|
9
9
|
* the new version back. `cause` preserves the underlying failure by identity
|
|
10
|
-
* when the failing step signals recoverable failure with `Effect.fail`.
|
|
11
|
-
* assembled all three into a prose `reason` string.
|
|
10
|
+
* when the failing step signals recoverable failure with `Effect.fail`.
|
|
12
11
|
*
|
|
13
12
|
* @public
|
|
14
13
|
*/
|
|
@@ -90,6 +89,23 @@ var ConfigMigration = class {
|
|
|
90
89
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
91
90
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
92
91
|
* in its error type.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { ConfigMigration, JsonCodec } from "@effected/config-file";
|
|
96
|
+
* import { Effect } from "effect";
|
|
97
|
+
*
|
|
98
|
+
* const codec = ConfigMigration.make({
|
|
99
|
+
* codec: JsonCodec,
|
|
100
|
+
* migrations: [
|
|
101
|
+
* {
|
|
102
|
+
* version: 2,
|
|
103
|
+
* name: "rename-host",
|
|
104
|
+
* up: (raw) => Effect.succeed({ ...(raw as object), host: (raw as { hostname?: string }).hostname }),
|
|
105
|
+
* },
|
|
106
|
+
* ],
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
93
109
|
*/
|
|
94
110
|
static make = make;
|
|
95
111
|
};
|
package/ConfigProvider.js
CHANGED
|
@@ -2,22 +2,21 @@ import { ConfigProvider, Effect } from "effect";
|
|
|
2
2
|
|
|
3
3
|
//#region src/ConfigProvider.ts
|
|
4
4
|
/**
|
|
5
|
-
* Expose a loaded, merged, schema-validated document as a
|
|
5
|
+
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
6
6
|
* so it can be read through `Config.String("port")` and layered beneath other
|
|
7
7
|
* providers.
|
|
8
8
|
*
|
|
9
9
|
* @remarks
|
|
10
10
|
* Strictly additive, and deliberately in its own module so it never becomes a
|
|
11
11
|
* required import. The schema-validated whole-document
|
|
12
|
-
* {@link ConfigFileShape.load} remains the primary API:
|
|
12
|
+
* {@link ConfigFileShape.load} remains the primary API: `Config` has no
|
|
13
13
|
* whole-document story, and this function is the bridge, not a replacement.
|
|
14
14
|
*
|
|
15
15
|
* Three properties, in order of how easy they are to lose:
|
|
16
16
|
*
|
|
17
17
|
* 1. **A missing file is a failure, not an empty provider.** The
|
|
18
18
|
* {@link ConfigFileNotFoundError} propagates. Handing back an empty provider
|
|
19
|
-
* would silently turn every subsequent `Config` read into "not found"
|
|
20
|
-
* is the exact class of lie this port exists to undo.
|
|
19
|
+
* would silently turn every subsequent `Config` read into "not found".
|
|
21
20
|
* 2. **Nested keys are structural, not dotted.** `fromUnknown` descends one path
|
|
22
21
|
* segment at a time, so `{ db: { host } }` is reached with
|
|
23
22
|
* `Config.nested(Config.String("host"), "db")` — never `Config.String("db.host")`,
|
|
@@ -45,8 +44,14 @@ import { ConfigProvider, Effect } from "effect";
|
|
|
45
44
|
*
|
|
46
45
|
* @example
|
|
47
46
|
* ```ts
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* import { asConfigProvider } from "@effected/config-file";
|
|
48
|
+
* import { ConfigProvider, Effect } from "effect";
|
|
49
|
+
*
|
|
50
|
+
* const program = Effect.gen(function* () {
|
|
51
|
+
* const cfg = yield* AppConfig; // a ConfigFile.Service class
|
|
52
|
+
* const fileProvider = yield* asConfigProvider(cfg);
|
|
53
|
+
* return ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
|
|
54
|
+
* });
|
|
50
55
|
* ```
|
|
51
56
|
*
|
|
52
57
|
* @public
|
|
@@ -57,9 +62,8 @@ const asConfigProvider = (service) => Effect.map(service.load, (value) => Config
|
|
|
57
62
|
* `ConfigProvider`, so `Config` accessors read env first and the file second.
|
|
58
63
|
*
|
|
59
64
|
* @remarks
|
|
60
|
-
* This
|
|
61
|
-
*
|
|
62
|
-
* for it, so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
65
|
+
* This layer composes beneath the **ambient** `ConfigProvider` (a
|
|
66
|
+
* `Context.Reference`), so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
63
67
|
* precedence explicitly: whatever that provider resolves wins, and the config
|
|
64
68
|
* file supplies whatever it lacks. That reference's own default is
|
|
65
69
|
* `ConfigProvider.fromEnv()`, which is what most applications want; this
|
package/EncryptedCodec.js
CHANGED
|
@@ -7,10 +7,9 @@ import { Duration, Effect, Exit, Schema } from "effect";
|
|
|
7
7
|
* failed.
|
|
8
8
|
*
|
|
9
9
|
* @remarks
|
|
10
|
-
* Its own error rather than
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* structurally; v3 assembled it into a prose `reason` string.
|
|
10
|
+
* Its own error rather than a value on the generic `ConfigCodecError.operation`
|
|
11
|
+
* union, so an encryption-only concern does not leak into every codec's error
|
|
12
|
+
* type. `cause` preserves the underlying host failure structurally.
|
|
14
13
|
*
|
|
15
14
|
* @public
|
|
16
15
|
*/
|
|
@@ -88,6 +87,14 @@ const keyEffect = (keySource) => keySource._tag === "CryptoKey" ? keySource.key
|
|
|
88
87
|
* flattening — the inner codec's failures stay distinguishable from
|
|
89
88
|
* cryptographic ones.
|
|
90
89
|
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
|
|
93
|
+
*
|
|
94
|
+
* const salt = new Uint8Array(16); // use a stored, random per-deployment salt
|
|
95
|
+
* const codec = EncryptedCodec(JsonCodec, EncryptedCodecKey.fromPassphrase("correct horse", salt));
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
91
98
|
* @public
|
|
92
99
|
*/
|
|
93
100
|
function EncryptedCodec(inner, keySource) {
|
package/MergeStrategy.js
CHANGED
|
@@ -36,11 +36,19 @@ const layeredMerge = () => ({
|
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
38
|
/**
|
|
39
|
-
* Built-in merge strategies
|
|
39
|
+
* Built-in merge strategies: `firstMatch` (the highest-priority source wins
|
|
40
|
+
* whole) and `layeredMerge` (every source deep-merged, higher priority winning).
|
|
40
41
|
*
|
|
41
42
|
* @remarks
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* Strategies only combine sources; the walk up the directory tree belongs to
|
|
44
|
+
* the `upwardWalk` resolver.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { MergeStrategy } from "@effected/config-file";
|
|
49
|
+
*
|
|
50
|
+
* const strategy = MergeStrategy.layeredMerge<{ port: number }>();
|
|
51
|
+
* ```
|
|
44
52
|
*
|
|
45
53
|
* @public
|
|
46
54
|
*/
|
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
Composable config file loading for Effect. Declare a resolver chain — an explicit path, an upward walk from the cwd, the workspace or git root, `/etc` — decode every discovered file through an Effect `Schema`, and combine the results with a merge strategy. JSON, JSONC, YAML and TOML all decode with no extra install. Codecs, resolvers and merge strategies are pluggable seams, and failures arrive as tagged errors carrying structured payloads rather than prose, so "no config anywhere" is routable separately from "the config I found is broken".
|
|
9
9
|
|
|
10
|
-
> **Pre
|
|
11
|
-
>
|
|
12
|
-
> `1.0.0`
|
|
13
|
-
>
|
|
10
|
+
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
+
> Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
|
|
12
|
+
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
|
+
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
14
|
> [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
|
|
15
15
|
>
|
|
16
16
|
> **Stability: unstable.** This package's API surface is not yet considered
|
package/index.d.ts
CHANGED
|
@@ -60,8 +60,11 @@ export declare class ConfigCodecError extends ConfigCodecError_base {
|
|
|
60
60
|
* @public
|
|
61
61
|
*/
|
|
62
62
|
interface ConfigCodec<E = ConfigCodecError> {
|
|
63
|
+
/** The codec's name, e.g. `"json"`; recorded on {@link ConfigCodecError} and in events. */
|
|
63
64
|
readonly name: string;
|
|
65
|
+
/** Turn file content into an unknown document. */
|
|
64
66
|
readonly parse: (raw: string) => Effect.Effect<unknown, E>;
|
|
67
|
+
/** Turn a document back into file content. */
|
|
65
68
|
readonly stringify: (value: unknown) => Effect.Effect<string, E>;
|
|
66
69
|
}
|
|
67
70
|
//#endregion
|
|
@@ -86,15 +89,14 @@ export declare const ConfigSourceRef: Schema.Struct<{
|
|
|
86
89
|
* persistence.
|
|
87
90
|
*
|
|
88
91
|
* @remarks
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* never observe a discovery failure to report one.
|
|
92
|
+
* There is no discovery-failure variant: under this package's
|
|
93
|
+
* resolver-absorption contract a resolver's error channel is `never` — every
|
|
94
|
+
* filesystem failure becomes `Option.none()` — so the pipeline never observes a
|
|
95
|
+
* discovery failure to report.
|
|
94
96
|
*
|
|
95
97
|
* The failure variants carry the **structured** typed error in `error`, not a
|
|
96
|
-
* `reason` string
|
|
97
|
-
*
|
|
98
|
+
* `reason` string, so every field a subscriber might branch on survives; a
|
|
99
|
+
* subscriber that wants prose can read `error.message`.
|
|
98
100
|
*
|
|
99
101
|
* @public
|
|
100
102
|
*/
|
|
@@ -260,7 +262,9 @@ export declare class ConfigEvents extends ConfigEvents_base {
|
|
|
260
262
|
* @public
|
|
261
263
|
*/
|
|
262
264
|
export interface ConfigResolver<R = never> {
|
|
265
|
+
/** The resolver's name, reported in `searched` lists, events and `ConfigSource.resolver`. */
|
|
263
266
|
readonly name: string;
|
|
267
|
+
/** Look up the config file's path: `Option.some(path)` when found, `Option.none()` otherwise. */
|
|
264
268
|
readonly resolve: Effect.Effect<Option.Option<string>, never, R>;
|
|
265
269
|
/**
|
|
266
270
|
* The same lookup, reporting **how** the file was found rather than only
|
|
@@ -511,15 +515,25 @@ type NonEmptySources<A> = readonly [ConfigSource<A>, ...ConfigSource<A>[]];
|
|
|
511
515
|
* @public
|
|
512
516
|
*/
|
|
513
517
|
export interface MergeStrategy<A> {
|
|
518
|
+
/** The strategy's name, reported on the `Resolved` event. */
|
|
514
519
|
readonly name: string;
|
|
520
|
+
/** Combine the sources, highest priority first, into one value. */
|
|
515
521
|
readonly resolve: (sources: NonEmptySources<A>) => Effect.Effect<A>;
|
|
516
522
|
}
|
|
517
523
|
/**
|
|
518
|
-
* Built-in merge strategies
|
|
524
|
+
* Built-in merge strategies: `firstMatch` (the highest-priority source wins
|
|
525
|
+
* whole) and `layeredMerge` (every source deep-merged, higher priority winning).
|
|
519
526
|
*
|
|
520
527
|
* @remarks
|
|
521
|
-
*
|
|
522
|
-
*
|
|
528
|
+
* Strategies only combine sources; the walk up the directory tree belongs to
|
|
529
|
+
* the `upwardWalk` resolver.
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* ```ts
|
|
533
|
+
* import { MergeStrategy } from "@effected/config-file";
|
|
534
|
+
*
|
|
535
|
+
* const strategy = MergeStrategy.layeredMerge<{ port: number }>();
|
|
536
|
+
* ```
|
|
523
537
|
*
|
|
524
538
|
* @public
|
|
525
539
|
*/
|
|
@@ -551,7 +565,7 @@ declare const ConfigFileNotFoundError_base: Schema.Class<ConfigFileNotFoundError
|
|
|
551
565
|
* @remarks
|
|
552
566
|
* Its own tag, so "no config anywhere" is routable with `Effect.catchTag`
|
|
553
567
|
* separately from "the config I found is broken" — the single most important
|
|
554
|
-
* distinction the
|
|
568
|
+
* distinction in the error set.
|
|
555
569
|
*
|
|
556
570
|
* @public
|
|
557
571
|
*/
|
|
@@ -568,8 +582,8 @@ declare const ConfigFileReadError_base: Schema.Class<ConfigFileReadError, Schema
|
|
|
568
582
|
* Indicates that a config file could not be read from the filesystem.
|
|
569
583
|
*
|
|
570
584
|
* @remarks
|
|
571
|
-
* `cause` preserves the underlying filesystem failure structurally
|
|
572
|
-
* it to
|
|
585
|
+
* `cause` preserves the underlying filesystem failure structurally rather than
|
|
586
|
+
* flattening it to a string.
|
|
573
587
|
*
|
|
574
588
|
* @public
|
|
575
589
|
*/
|
|
@@ -596,13 +610,12 @@ declare const ConfigDefaultPathMissingError_base: Schema.Class<ConfigDefaultPath
|
|
|
596
610
|
* was called on a service configured without a `defaultPath`.
|
|
597
611
|
*
|
|
598
612
|
* @remarks
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* write failure.
|
|
613
|
+
* It is distinct by tag from a real {@link ConfigFileWriteError}, so a caller
|
|
614
|
+
* can tell "no destination was configured" from "the write failed".
|
|
602
615
|
*
|
|
603
616
|
* It carries no `path` field on purpose. The whole point of this failure is
|
|
604
617
|
* that there is no path; a {@link ConfigFileWriteError} with a fabricated path
|
|
605
|
-
* would be a lie
|
|
618
|
+
* would be a lie.
|
|
606
619
|
*
|
|
607
620
|
* @public
|
|
608
621
|
*/
|
|
@@ -621,10 +634,9 @@ declare const ConfigValidationError_base: Schema.Class<ConfigValidationError, Sc
|
|
|
621
634
|
*
|
|
622
635
|
* @remarks
|
|
623
636
|
* `issue` carries the **structured** schema failure — at runtime a
|
|
624
|
-
* `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* narrow it with the `SchemaIssue` module.
|
|
637
|
+
* `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues`, so
|
|
638
|
+
* every field a caller might branch on survives. It is typed `unknown` because
|
|
639
|
+
* there is no `Schema` for `Issue`; narrow it with the `SchemaIssue` module.
|
|
628
640
|
*
|
|
629
641
|
* @public
|
|
630
642
|
*/
|
|
@@ -723,7 +735,7 @@ interface ConfigFileShape<A> {
|
|
|
723
735
|
* @remarks
|
|
724
736
|
* A found-but-corrupt source ABORTS discovery with a typed error rather than
|
|
725
737
|
* being silently skipped: silently skipping a corrupt file would mean running
|
|
726
|
-
* on the wrong config. This is deliberate
|
|
738
|
+
* on the wrong config. This is deliberate.
|
|
727
739
|
*/
|
|
728
740
|
readonly discover: Effect.Effect<ReadonlyArray<ConfigSource<A>>, ConfigReadError>;
|
|
729
741
|
/**
|
|
@@ -786,7 +798,7 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
786
798
|
* The schema every discovered document is decoded through.
|
|
787
799
|
*
|
|
788
800
|
* @remarks
|
|
789
|
-
* `Schema.Codec<A, I>` rather than
|
|
801
|
+
* `Schema.Codec<A, I>` rather than the one-parameter `Schema.Schema<A>`,
|
|
790
802
|
* because the encoded form `I` matters on the write path. Its decoding and
|
|
791
803
|
* encoding service channels default to `never`, keeping `decode` free of
|
|
792
804
|
* requirements.
|
|
@@ -830,8 +842,6 @@ interface ConfigFileOptions<A, I, RR> {
|
|
|
830
842
|
*
|
|
831
843
|
* @remarks
|
|
832
844
|
* Its requirements join the resolvers' in `RR` and flow into the layer's `R`.
|
|
833
|
-
* v3 typed this `Effect<string, ConfigError, any>` and cast the requirements
|
|
834
|
-
* away at the call site.
|
|
835
845
|
*
|
|
836
846
|
* When absent, `save` and `update` fail with
|
|
837
847
|
* {@link ConfigDefaultPathMissingError}.
|
|
@@ -931,14 +941,30 @@ export declare class ConfigFile {
|
|
|
931
941
|
* Build the live layer for a config service class.
|
|
932
942
|
*
|
|
933
943
|
* @remarks
|
|
934
|
-
* Resolver requirements flow into the layer's `R` type
|
|
935
|
-
* `
|
|
936
|
-
*
|
|
944
|
+
* Resolver requirements flow into the layer's `R` type: the result is
|
|
945
|
+
* `Layer<Self, never, FileSystem | Path | RR>`, so the platform services and
|
|
946
|
+
* every resolver's needs are visible at the provide site.
|
|
937
947
|
*
|
|
938
948
|
* `ConfigFile.layer` is a layer-RETURNING function, not a layer: calling it
|
|
939
949
|
* twice builds two independent service instances. Bind its result to a const
|
|
940
|
-
* and provide that const
|
|
941
|
-
*
|
|
950
|
+
* and provide that const — do not call `ConfigFile.layer(...)` inline at each
|
|
951
|
+
* provide site.
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```ts
|
|
955
|
+
* import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
|
|
956
|
+
* import { Schema } from "effect";
|
|
957
|
+
*
|
|
958
|
+
* const AppShape = Schema.Struct({ port: Schema.Number });
|
|
959
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
|
|
960
|
+
*
|
|
961
|
+
* const AppConfigLive = ConfigFile.layer(AppConfig, {
|
|
962
|
+
* schema: AppShape,
|
|
963
|
+
* codec: JsonCodec,
|
|
964
|
+
* resolvers: [ConfigResolver.explicitPath("./app.config.json")],
|
|
965
|
+
* strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
|
|
966
|
+
* });
|
|
967
|
+
* ```
|
|
942
968
|
*/
|
|
943
969
|
static readonly layer: <Self, A, I, RR = never>(tag: Context.Key<Self, ConfigFileShape<A>>, options: ConfigFileOptions<A, I, RR>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | RR>;
|
|
944
970
|
/**
|
|
@@ -957,10 +983,9 @@ export declare class ConfigFile {
|
|
|
957
983
|
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
958
984
|
* `node:fs`.
|
|
959
985
|
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
* layer's requirements.
|
|
986
|
+
* The temp directory is removed by a finalizer bound to the layer's own scope,
|
|
987
|
+
* so cleanup runs on release without surfacing `Scope` in the layer's
|
|
988
|
+
* requirements.
|
|
964
989
|
*
|
|
965
990
|
* @example
|
|
966
991
|
* ```ts
|
|
@@ -996,10 +1021,17 @@ export declare class ConfigFile {
|
|
|
996
1021
|
* @example
|
|
997
1022
|
* ```ts
|
|
998
1023
|
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
1024
|
+
* import { Effect, Schema } from "effect";
|
|
1025
|
+
*
|
|
1026
|
+
* const MyConfig = Schema.Struct({ port: Schema.Number });
|
|
999
1027
|
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1028
|
+
* // Requires `FileSystem` in `R`; provide it from a platform layer.
|
|
1029
|
+
* const program = Effect.gen(function* () {
|
|
1030
|
+
* const config = yield* ConfigFile.read("./app.config.json", {
|
|
1031
|
+
* schema: MyConfig,
|
|
1032
|
+
* codec: JsonCodec,
|
|
1033
|
+
* });
|
|
1034
|
+
* return config.port;
|
|
1003
1035
|
* });
|
|
1004
1036
|
* ```
|
|
1005
1037
|
*/
|
|
@@ -1023,8 +1055,7 @@ declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Sche
|
|
|
1023
1055
|
* @remarks
|
|
1024
1056
|
* `phase` says where: reading the current version, applying a step, or writing
|
|
1025
1057
|
* the new version back. `cause` preserves the underlying failure by identity
|
|
1026
|
-
* when the failing step signals recoverable failure with `Effect.fail`.
|
|
1027
|
-
* assembled all three into a prose `reason` string.
|
|
1058
|
+
* when the failing step signals recoverable failure with `Effect.fail`.
|
|
1028
1059
|
*
|
|
1029
1060
|
* @public
|
|
1030
1061
|
*/
|
|
@@ -1035,13 +1066,14 @@ export declare class ConfigMigrationError extends ConfigMigrationError_base {
|
|
|
1035
1066
|
* A single versioned migration step.
|
|
1036
1067
|
*
|
|
1037
1068
|
* @remarks
|
|
1038
|
-
*
|
|
1039
|
-
* the codebase, so it is not ported.
|
|
1069
|
+
* Steps are forward-only: there is no reverse (`down`) migration.
|
|
1040
1070
|
*
|
|
1041
1071
|
* @public
|
|
1042
1072
|
*/
|
|
1043
1073
|
interface ConfigFileMigration {
|
|
1074
|
+
/** The version this step migrates the config to. Steps run in ascending order. */
|
|
1044
1075
|
readonly version: number;
|
|
1076
|
+
/** A label for the step, carried on {@link ConfigMigrationError} when it fails. */
|
|
1045
1077
|
readonly name: string;
|
|
1046
1078
|
/**
|
|
1047
1079
|
* Transforms the parsed config. Signal recoverable failure with `Effect.fail`;
|
|
@@ -1051,7 +1083,9 @@ interface ConfigFileMigration {
|
|
|
1051
1083
|
}
|
|
1052
1084
|
/** How the version number is read from and written to the parsed config. @public */
|
|
1053
1085
|
export interface VersionAccess {
|
|
1086
|
+
/** Read the current version from the parsed config. */
|
|
1054
1087
|
readonly get: (raw: unknown) => Effect.Effect<number, unknown>;
|
|
1088
|
+
/** Return the config with `version` written back. */
|
|
1055
1089
|
readonly set: (raw: unknown, version: number) => Effect.Effect<unknown, unknown>;
|
|
1056
1090
|
}
|
|
1057
1091
|
/** Reads and writes a top-level `version` field. @public */
|
|
@@ -1060,8 +1094,11 @@ export declare const VersionAccess: {
|
|
|
1060
1094
|
};
|
|
1061
1095
|
/** Options for {@link ConfigMigration.make}. @public */
|
|
1062
1096
|
interface ConfigMigrationOptions {
|
|
1097
|
+
/** The codec being wrapped. */
|
|
1063
1098
|
readonly codec: ConfigCodec;
|
|
1099
|
+
/** The migration steps; they run in ascending `version` order. */
|
|
1064
1100
|
readonly migrations: ReadonlyArray<ConfigFileMigration>;
|
|
1101
|
+
/** How the version is read and written; defaults to {@link (VersionAccess:variable).default}, a top-level `version` field. */
|
|
1065
1102
|
readonly versionAccess?: VersionAccess;
|
|
1066
1103
|
}
|
|
1067
1104
|
/** Versioned migration support for config codecs. @public */
|
|
@@ -1075,28 +1112,44 @@ export declare class ConfigMigration {
|
|
|
1075
1112
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
1076
1113
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
1077
1114
|
* in its error type.
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* import { ConfigMigration, JsonCodec } from "@effected/config-file";
|
|
1119
|
+
* import { Effect } from "effect";
|
|
1120
|
+
*
|
|
1121
|
+
* const codec = ConfigMigration.make({
|
|
1122
|
+
* codec: JsonCodec,
|
|
1123
|
+
* migrations: [
|
|
1124
|
+
* {
|
|
1125
|
+
* version: 2,
|
|
1126
|
+
* name: "rename-host",
|
|
1127
|
+
* up: (raw) => Effect.succeed({ ...(raw as object), host: (raw as { hostname?: string }).hostname }),
|
|
1128
|
+
* },
|
|
1129
|
+
* ],
|
|
1130
|
+
* });
|
|
1131
|
+
* ```
|
|
1078
1132
|
*/
|
|
1079
1133
|
static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<ConfigCodecError | ConfigMigrationError>;
|
|
1080
1134
|
}
|
|
1081
1135
|
//#endregion
|
|
1082
1136
|
//#region src/ConfigProvider.d.ts
|
|
1083
1137
|
/**
|
|
1084
|
-
* Expose a loaded, merged, schema-validated document as a
|
|
1138
|
+
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
1085
1139
|
* so it can be read through `Config.String("port")` and layered beneath other
|
|
1086
1140
|
* providers.
|
|
1087
1141
|
*
|
|
1088
1142
|
* @remarks
|
|
1089
1143
|
* Strictly additive, and deliberately in its own module so it never becomes a
|
|
1090
1144
|
* required import. The schema-validated whole-document
|
|
1091
|
-
* {@link ConfigFileShape.load} remains the primary API:
|
|
1145
|
+
* {@link ConfigFileShape.load} remains the primary API: `Config` has no
|
|
1092
1146
|
* whole-document story, and this function is the bridge, not a replacement.
|
|
1093
1147
|
*
|
|
1094
1148
|
* Three properties, in order of how easy they are to lose:
|
|
1095
1149
|
*
|
|
1096
1150
|
* 1. **A missing file is a failure, not an empty provider.** The
|
|
1097
1151
|
* {@link ConfigFileNotFoundError} propagates. Handing back an empty provider
|
|
1098
|
-
* would silently turn every subsequent `Config` read into "not found"
|
|
1099
|
-
* is the exact class of lie this port exists to undo.
|
|
1152
|
+
* would silently turn every subsequent `Config` read into "not found".
|
|
1100
1153
|
* 2. **Nested keys are structural, not dotted.** `fromUnknown` descends one path
|
|
1101
1154
|
* segment at a time, so `{ db: { host } }` is reached with
|
|
1102
1155
|
* `Config.nested(Config.String("host"), "db")` — never `Config.String("db.host")`,
|
|
@@ -1124,8 +1177,14 @@ export declare class ConfigMigration {
|
|
|
1124
1177
|
*
|
|
1125
1178
|
* @example
|
|
1126
1179
|
* ```ts
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1180
|
+
* import { asConfigProvider } from "@effected/config-file";
|
|
1181
|
+
* import { ConfigProvider, Effect } from "effect";
|
|
1182
|
+
*
|
|
1183
|
+
* const program = Effect.gen(function* () {
|
|
1184
|
+
* const cfg = yield* AppConfig; // a ConfigFile.Service class
|
|
1185
|
+
* const fileProvider = yield* asConfigProvider(cfg);
|
|
1186
|
+
* return ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
|
|
1187
|
+
* });
|
|
1129
1188
|
* ```
|
|
1130
1189
|
*
|
|
1131
1190
|
* @public
|
|
@@ -1152,9 +1211,8 @@ interface LayerConfigProviderOptions {
|
|
|
1152
1211
|
* `ConfigProvider`, so `Config` accessors read env first and the file second.
|
|
1153
1212
|
*
|
|
1154
1213
|
* @remarks
|
|
1155
|
-
* This
|
|
1156
|
-
*
|
|
1157
|
-
* for it, so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
1214
|
+
* This layer composes beneath the **ambient** `ConfigProvider` (a
|
|
1215
|
+
* `Context.Reference`), so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
1158
1216
|
* precedence explicitly: whatever that provider resolves wins, and the config
|
|
1159
1217
|
* file supplies whatever it lacks. That reference's own default is
|
|
1160
1218
|
* `ConfigProvider.fromEnv()`, which is what most applications want; this
|
|
@@ -1187,10 +1245,9 @@ declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Sc
|
|
|
1187
1245
|
* failed.
|
|
1188
1246
|
*
|
|
1189
1247
|
* @remarks
|
|
1190
|
-
* Its own error rather than
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
* structurally; v3 assembled it into a prose `reason` string.
|
|
1248
|
+
* Its own error rather than a value on the generic `ConfigCodecError.operation`
|
|
1249
|
+
* union, so an encryption-only concern does not leak into every codec's error
|
|
1250
|
+
* type. `cause` preserves the underlying host failure structurally.
|
|
1194
1251
|
*
|
|
1195
1252
|
* @public
|
|
1196
1253
|
*/
|
|
@@ -1260,6 +1317,14 @@ export declare const EncryptedCodecKey: {
|
|
|
1260
1317
|
* flattening — the inner codec's failures stay distinguishable from
|
|
1261
1318
|
* cryptographic ones.
|
|
1262
1319
|
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* ```ts
|
|
1322
|
+
* import { EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
|
|
1323
|
+
*
|
|
1324
|
+
* const salt = new Uint8Array(16); // use a stored, random per-deployment salt
|
|
1325
|
+
* const codec = EncryptedCodec(JsonCodec, EncryptedCodecKey.fromPassphrase("correct horse", salt));
|
|
1326
|
+
* ```
|
|
1327
|
+
*
|
|
1263
1328
|
* @public
|
|
1264
1329
|
*/
|
|
1265
1330
|
export declare function EncryptedCodec<E>(inner: ConfigCodec<E>, keySource: EncryptedCodecKey): ConfigCodec<E | ConfigEncryptionError>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/config-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,11 +43,12 @@
|
|
|
43
43
|
"./package.json": "./package.json"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@effected/
|
|
47
|
-
"@effected/
|
|
48
|
-
"@effected/
|
|
49
|
-
"@effected/
|
|
50
|
-
"
|
|
46
|
+
"@effected/glob": "^0.10.0",
|
|
47
|
+
"@effected/jsonc": "^0.15.0",
|
|
48
|
+
"@effected/toml": "^0.11.0",
|
|
49
|
+
"@effected/walker": "^0.15.0",
|
|
50
|
+
"@effected/yaml": "^0.19.0",
|
|
51
|
+
"effect": "^4.0.0"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=24.11.0"
|