@effected/config-file 0.13.1 → 0.14.1
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 +61 -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 +132 -56
- 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,38 @@ 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
|
+
* `tag` is a {@link ConfigFile.Service} key — its service type
|
|
470
|
+
* `ConfigFileShape<A>` for the schema's `A`. A key whose shape adds members
|
|
471
|
+
* (`ConfigFileShape<A> & { … }`) is a compile error, reported as an argument
|
|
472
|
+
* "not assignable to parameter of type 'never'", because this layer could not
|
|
473
|
+
* supply them. The check cannot see through method-syntax parameter
|
|
474
|
+
* bivariance: a member redeclared as a method with a wider parameter still
|
|
475
|
+
* compiles.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
|
|
480
|
+
* import { Schema } from "effect";
|
|
481
|
+
*
|
|
482
|
+
* const AppShape = Schema.Struct({ port: Schema.Number });
|
|
483
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
|
|
484
|
+
*
|
|
485
|
+
* const AppConfigLive = ConfigFile.layer(AppConfig, {
|
|
486
|
+
* schema: AppShape,
|
|
487
|
+
* codec: JsonCodec,
|
|
488
|
+
* resolvers: [ConfigResolver.explicitPath("./app.config.json")],
|
|
489
|
+
* strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
|
|
490
|
+
* });
|
|
491
|
+
* ```
|
|
470
492
|
*/
|
|
471
493
|
static layer = layer;
|
|
472
494
|
/**
|
|
@@ -485,10 +507,12 @@ var ConfigFile = class {
|
|
|
485
507
|
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
486
508
|
* `node:fs`.
|
|
487
509
|
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
510
|
+
* The temp directory is removed by a finalizer bound to the layer's own scope,
|
|
511
|
+
* so cleanup runs on release without surfacing `Scope` in the layer's
|
|
512
|
+
* requirements.
|
|
513
|
+
*
|
|
514
|
+
* `tag` takes the same `ConfigFileShape<A>` key as {@link ConfigFile.layer},
|
|
515
|
+
* with the same check: a shape that adds members is a compile error.
|
|
492
516
|
*
|
|
493
517
|
* @example
|
|
494
518
|
* ```ts
|
|
@@ -524,10 +548,17 @@ var ConfigFile = class {
|
|
|
524
548
|
* @example
|
|
525
549
|
* ```ts
|
|
526
550
|
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
551
|
+
* import { Effect, Schema } from "effect";
|
|
552
|
+
*
|
|
553
|
+
* const MyConfig = Schema.Struct({ port: Schema.Number });
|
|
527
554
|
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
555
|
+
* // Requires `FileSystem` in `R`; provide it from a platform layer.
|
|
556
|
+
* const program = Effect.gen(function* () {
|
|
557
|
+
* const config = yield* ConfigFile.read("./app.config.json", {
|
|
558
|
+
* schema: MyConfig,
|
|
559
|
+
* codec: JsonCodec,
|
|
560
|
+
* });
|
|
561
|
+
* return config.port;
|
|
531
562
|
* });
|
|
532
563
|
* ```
|
|
533
564
|
*/
|
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,16 +941,40 @@ 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
|
+
* `tag` is a {@link ConfigFile.Service} key — its service type
|
|
954
|
+
* `ConfigFileShape<A>` for the schema's `A`. A key whose shape adds members
|
|
955
|
+
* (`ConfigFileShape<A> & { … }`) is a compile error, reported as an argument
|
|
956
|
+
* "not assignable to parameter of type 'never'", because this layer could not
|
|
957
|
+
* supply them. The check cannot see through method-syntax parameter
|
|
958
|
+
* bivariance: a member redeclared as a method with a wider parameter still
|
|
959
|
+
* compiles.
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```ts
|
|
963
|
+
* import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
|
|
964
|
+
* import { Schema } from "effect";
|
|
965
|
+
*
|
|
966
|
+
* const AppShape = Schema.Struct({ port: Schema.Number });
|
|
967
|
+
* class AppConfig extends ConfigFile.Service<AppConfig, typeof AppShape.Type>()("app/Config") {}
|
|
968
|
+
*
|
|
969
|
+
* const AppConfigLive = ConfigFile.layer(AppConfig, {
|
|
970
|
+
* schema: AppShape,
|
|
971
|
+
* codec: JsonCodec,
|
|
972
|
+
* resolvers: [ConfigResolver.explicitPath("./app.config.json")],
|
|
973
|
+
* strategy: MergeStrategy.firstMatch<typeof AppShape.Type>(),
|
|
974
|
+
* });
|
|
975
|
+
* ```
|
|
942
976
|
*/
|
|
943
|
-
static readonly layer: <Self, A, I, RR = never>(tag: Context.Key<Self, ConfigFileShape<A
|
|
977
|
+
static readonly layer: <Self, A, I, RR = never, S extends ConfigFileShape<A> = ConfigFileShape<A>>(tag: Context.Key<Self, S> & ([ConfigFileShape<A>] extends [S] ? unknown : never), options: ConfigFileOptions<A, I, RR>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path | RR>;
|
|
944
978
|
/**
|
|
945
979
|
* A scoped layer that seeds `files` into a temp directory, wires the **real**
|
|
946
980
|
* live implementation over them, and removes the directory when the scope
|
|
@@ -957,10 +991,12 @@ export declare class ConfigFile {
|
|
|
957
991
|
* directory is created through `FileSystem.makeTempDirectory` rather than
|
|
958
992
|
* `node:fs`.
|
|
959
993
|
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
994
|
+
* The temp directory is removed by a finalizer bound to the layer's own scope,
|
|
995
|
+
* so cleanup runs on release without surfacing `Scope` in the layer's
|
|
996
|
+
* requirements.
|
|
997
|
+
*
|
|
998
|
+
* `tag` takes the same `ConfigFileShape<A>` key as {@link ConfigFile.layer},
|
|
999
|
+
* with the same check: a shape that adds members is a compile error.
|
|
964
1000
|
*
|
|
965
1001
|
* @example
|
|
966
1002
|
* ```ts
|
|
@@ -972,7 +1008,7 @@ export declare class ConfigFile {
|
|
|
972
1008
|
* }).pipe(Layer.provide(NodeServices.layer));
|
|
973
1009
|
* ```
|
|
974
1010
|
*/
|
|
975
|
-
static readonly testLayer: <Self, A, I>(tag: Context.Key<Self, ConfigFileShape<A
|
|
1011
|
+
static readonly testLayer: <Self, A, I, S extends ConfigFileShape<A> = ConfigFileShape<A>>(tag: Context.Key<Self, S> & ([ConfigFileShape<A>] extends [S] ? unknown : never), options: ConfigFileTestOptions<A, I>) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path>;
|
|
976
1012
|
/**
|
|
977
1013
|
* Read, decode and validate one explicit path — no service, no layer, no tag.
|
|
978
1014
|
*
|
|
@@ -996,10 +1032,17 @@ export declare class ConfigFile {
|
|
|
996
1032
|
* @example
|
|
997
1033
|
* ```ts
|
|
998
1034
|
* import { ConfigFile, JsonCodec } from "@effected/config-file";
|
|
1035
|
+
* import { Effect, Schema } from "effect";
|
|
999
1036
|
*
|
|
1000
|
-
* const
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1037
|
+
* const MyConfig = Schema.Struct({ port: Schema.Number });
|
|
1038
|
+
*
|
|
1039
|
+
* // Requires `FileSystem` in `R`; provide it from a platform layer.
|
|
1040
|
+
* const program = Effect.gen(function* () {
|
|
1041
|
+
* const config = yield* ConfigFile.read("./app.config.json", {
|
|
1042
|
+
* schema: MyConfig,
|
|
1043
|
+
* codec: JsonCodec,
|
|
1044
|
+
* });
|
|
1045
|
+
* return config.port;
|
|
1003
1046
|
* });
|
|
1004
1047
|
* ```
|
|
1005
1048
|
*/
|
|
@@ -1023,8 +1066,7 @@ declare const ConfigMigrationError_base: Schema.Class<ConfigMigrationError, Sche
|
|
|
1023
1066
|
* @remarks
|
|
1024
1067
|
* `phase` says where: reading the current version, applying a step, or writing
|
|
1025
1068
|
* 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.
|
|
1069
|
+
* when the failing step signals recoverable failure with `Effect.fail`.
|
|
1028
1070
|
*
|
|
1029
1071
|
* @public
|
|
1030
1072
|
*/
|
|
@@ -1035,13 +1077,14 @@ export declare class ConfigMigrationError extends ConfigMigrationError_base {
|
|
|
1035
1077
|
* A single versioned migration step.
|
|
1036
1078
|
*
|
|
1037
1079
|
* @remarks
|
|
1038
|
-
*
|
|
1039
|
-
* the codebase, so it is not ported.
|
|
1080
|
+
* Steps are forward-only: there is no reverse (`down`) migration.
|
|
1040
1081
|
*
|
|
1041
1082
|
* @public
|
|
1042
1083
|
*/
|
|
1043
1084
|
interface ConfigFileMigration {
|
|
1085
|
+
/** The version this step migrates the config to. Steps run in ascending order. */
|
|
1044
1086
|
readonly version: number;
|
|
1087
|
+
/** A label for the step, carried on {@link ConfigMigrationError} when it fails. */
|
|
1045
1088
|
readonly name: string;
|
|
1046
1089
|
/**
|
|
1047
1090
|
* Transforms the parsed config. Signal recoverable failure with `Effect.fail`;
|
|
@@ -1051,7 +1094,9 @@ interface ConfigFileMigration {
|
|
|
1051
1094
|
}
|
|
1052
1095
|
/** How the version number is read from and written to the parsed config. @public */
|
|
1053
1096
|
export interface VersionAccess {
|
|
1097
|
+
/** Read the current version from the parsed config. */
|
|
1054
1098
|
readonly get: (raw: unknown) => Effect.Effect<number, unknown>;
|
|
1099
|
+
/** Return the config with `version` written back. */
|
|
1055
1100
|
readonly set: (raw: unknown, version: number) => Effect.Effect<unknown, unknown>;
|
|
1056
1101
|
}
|
|
1057
1102
|
/** Reads and writes a top-level `version` field. @public */
|
|
@@ -1060,8 +1105,11 @@ export declare const VersionAccess: {
|
|
|
1060
1105
|
};
|
|
1061
1106
|
/** Options for {@link ConfigMigration.make}. @public */
|
|
1062
1107
|
interface ConfigMigrationOptions {
|
|
1108
|
+
/** The codec being wrapped. */
|
|
1063
1109
|
readonly codec: ConfigCodec;
|
|
1110
|
+
/** The migration steps; they run in ascending `version` order. */
|
|
1064
1111
|
readonly migrations: ReadonlyArray<ConfigFileMigration>;
|
|
1112
|
+
/** How the version is read and written; defaults to {@link (VersionAccess:variable).default}, a top-level `version` field. */
|
|
1065
1113
|
readonly versionAccess?: VersionAccess;
|
|
1066
1114
|
}
|
|
1067
1115
|
/** Versioned migration support for config codecs. @public */
|
|
@@ -1075,28 +1123,44 @@ export declare class ConfigMigration {
|
|
|
1075
1123
|
* {@link ConfigMigrationError} rather than flattening migration failures into
|
|
1076
1124
|
* the inner codec's error — the reason the {@link (ConfigCodec:interface)} seam is generic
|
|
1077
1125
|
* in its error type.
|
|
1126
|
+
*
|
|
1127
|
+
* @example
|
|
1128
|
+
* ```ts
|
|
1129
|
+
* import { ConfigMigration, JsonCodec } from "@effected/config-file";
|
|
1130
|
+
* import { Effect } from "effect";
|
|
1131
|
+
*
|
|
1132
|
+
* const codec = ConfigMigration.make({
|
|
1133
|
+
* codec: JsonCodec,
|
|
1134
|
+
* migrations: [
|
|
1135
|
+
* {
|
|
1136
|
+
* version: 2,
|
|
1137
|
+
* name: "rename-host",
|
|
1138
|
+
* up: (raw) => Effect.succeed({ ...(raw as object), host: (raw as { hostname?: string }).hostname }),
|
|
1139
|
+
* },
|
|
1140
|
+
* ],
|
|
1141
|
+
* });
|
|
1142
|
+
* ```
|
|
1078
1143
|
*/
|
|
1079
1144
|
static readonly make: (options: ConfigMigrationOptions) => ConfigCodec<ConfigCodecError | ConfigMigrationError>;
|
|
1080
1145
|
}
|
|
1081
1146
|
//#endregion
|
|
1082
1147
|
//#region src/ConfigProvider.d.ts
|
|
1083
1148
|
/**
|
|
1084
|
-
* Expose a loaded, merged, schema-validated document as a
|
|
1149
|
+
* Expose a loaded, merged, schema-validated document as a `ConfigProvider`,
|
|
1085
1150
|
* so it can be read through `Config.String("port")` and layered beneath other
|
|
1086
1151
|
* providers.
|
|
1087
1152
|
*
|
|
1088
1153
|
* @remarks
|
|
1089
1154
|
* Strictly additive, and deliberately in its own module so it never becomes a
|
|
1090
1155
|
* required import. The schema-validated whole-document
|
|
1091
|
-
* {@link ConfigFileShape.load} remains the primary API:
|
|
1156
|
+
* {@link ConfigFileShape.load} remains the primary API: `Config` has no
|
|
1092
1157
|
* whole-document story, and this function is the bridge, not a replacement.
|
|
1093
1158
|
*
|
|
1094
1159
|
* Three properties, in order of how easy they are to lose:
|
|
1095
1160
|
*
|
|
1096
1161
|
* 1. **A missing file is a failure, not an empty provider.** The
|
|
1097
1162
|
* {@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.
|
|
1163
|
+
* would silently turn every subsequent `Config` read into "not found".
|
|
1100
1164
|
* 2. **Nested keys are structural, not dotted.** `fromUnknown` descends one path
|
|
1101
1165
|
* segment at a time, so `{ db: { host } }` is reached with
|
|
1102
1166
|
* `Config.nested(Config.String("host"), "db")` — never `Config.String("db.host")`,
|
|
@@ -1124,8 +1188,14 @@ export declare class ConfigMigration {
|
|
|
1124
1188
|
*
|
|
1125
1189
|
* @example
|
|
1126
1190
|
* ```ts
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1191
|
+
* import { asConfigProvider } from "@effected/config-file";
|
|
1192
|
+
* import { ConfigProvider, Effect } from "effect";
|
|
1193
|
+
*
|
|
1194
|
+
* const program = Effect.gen(function* () {
|
|
1195
|
+
* const cfg = yield* AppConfig; // a ConfigFile.Service class
|
|
1196
|
+
* const fileProvider = yield* asConfigProvider(cfg);
|
|
1197
|
+
* return ConfigProvider.orElse(ConfigProvider.fromEnv(), fileProvider);
|
|
1198
|
+
* });
|
|
1129
1199
|
* ```
|
|
1130
1200
|
*
|
|
1131
1201
|
* @public
|
|
@@ -1152,9 +1222,8 @@ interface LayerConfigProviderOptions {
|
|
|
1152
1222
|
* `ConfigProvider`, so `Config` accessors read env first and the file second.
|
|
1153
1223
|
*
|
|
1154
1224
|
* @remarks
|
|
1155
|
-
* This
|
|
1156
|
-
*
|
|
1157
|
-
* for it, so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
1225
|
+
* This layer composes beneath the **ambient** `ConfigProvider` (a
|
|
1226
|
+
* `Context.Reference`), so a consumer supplying `ConfigProvider.layer(...)` controls
|
|
1158
1227
|
* precedence explicitly: whatever that provider resolves wins, and the config
|
|
1159
1228
|
* file supplies whatever it lacks. That reference's own default is
|
|
1160
1229
|
* `ConfigProvider.fromEnv()`, which is what most applications want; this
|
|
@@ -1187,10 +1256,9 @@ declare const ConfigEncryptionError_base: Schema.Class<ConfigEncryptionError, Sc
|
|
|
1187
1256
|
* failed.
|
|
1188
1257
|
*
|
|
1189
1258
|
* @remarks
|
|
1190
|
-
* Its own error rather than
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
* structurally; v3 assembled it into a prose `reason` string.
|
|
1259
|
+
* Its own error rather than a value on the generic `ConfigCodecError.operation`
|
|
1260
|
+
* union, so an encryption-only concern does not leak into every codec's error
|
|
1261
|
+
* type. `cause` preserves the underlying host failure structurally.
|
|
1194
1262
|
*
|
|
1195
1263
|
* @public
|
|
1196
1264
|
*/
|
|
@@ -1260,6 +1328,14 @@ export declare const EncryptedCodecKey: {
|
|
|
1260
1328
|
* flattening — the inner codec's failures stay distinguishable from
|
|
1261
1329
|
* cryptographic ones.
|
|
1262
1330
|
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* import { EncryptedCodec, EncryptedCodecKey, JsonCodec } from "@effected/config-file";
|
|
1334
|
+
*
|
|
1335
|
+
* const salt = new Uint8Array(16); // use a stored, random per-deployment salt
|
|
1336
|
+
* const codec = EncryptedCodec(JsonCodec, EncryptedCodecKey.fromPassphrase("correct horse", salt));
|
|
1337
|
+
* ```
|
|
1338
|
+
*
|
|
1263
1339
|
* @public
|
|
1264
1340
|
*/
|
|
1265
1341
|
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.1",
|
|
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"
|