@velajs/vela 1.14.0 → 1.16.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/CHANGELOG.md +25 -0
- package/dist/application.d.ts +10 -1
- package/dist/application.js +12 -0
- package/dist/config/config.module.d.ts +8 -1
- package/dist/config/config.module.js +89 -42
- package/dist/config/config.service.d.ts +17 -4
- package/dist/config/config.service.js +18 -20
- package/dist/config/config.store.d.ts +46 -0
- package/dist/config/config.store.js +119 -0
- package/dist/config/config.tokens.d.ts +11 -0
- package/dist/config/config.tokens.js +12 -0
- package/dist/config/config.types.d.ts +37 -1
- package/dist/config/config.types.js +4 -1
- package/dist/config/index.d.ts +5 -2
- package/dist/config/index.js +3 -1
- package/dist/config/register-as.d.ts +52 -0
- package/dist/config/register-as.js +39 -0
- package/dist/container/container.d.ts +8 -1
- package/dist/container/container.js +39 -1
- package/dist/container/index.d.ts +2 -2
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +23 -0
- package/dist/container/types.js +6 -1
- package/dist/{storage → crypto}/signed-url.js +5 -0
- package/dist/factory/bootstrap.js +10 -0
- package/dist/http/decorators.d.ts +18 -9
- package/dist/http/decorators.js +4 -1
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +2 -0
- package/dist/http/route-map.d.ts +30 -0
- package/dist/http/route-map.js +27 -0
- package/dist/http/route.manager.d.ts +24 -0
- package/dist/http/route.manager.js +28 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/url/index.d.ts +4 -0
- package/dist/http/url/index.js +3 -0
- package/dist/http/url/signed-url.guard.d.ts +27 -0
- package/dist/http/url/signed-url.guard.js +62 -0
- package/dist/http/url/signing-secret.d.ts +20 -0
- package/dist/http/url/signing-secret.js +24 -0
- package/dist/http/url/url-generator.service.d.ts +52 -0
- package/dist/http/url/url-generator.service.js +104 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +5 -3
- package/dist/openapi/document.js +2 -1
- package/dist/registry/types.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +4 -1
- package/package.json +6 -2
- /package/dist/{storage → crypto}/signed-url.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.15.0 (2026-07-04)
|
|
4
|
+
|
|
5
|
+
Introspection seams for `@velajs/cli` (roadmap phase 3, CLI introspection):
|
|
6
|
+
serializable, zero-instantiation views of the app the CLI renders instead of
|
|
7
|
+
re-deriving framework internals.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`app.describeRoutes(): RouteDescription[]`** — every explicit controller
|
|
12
|
+
route exactly as `build()` registered it: method AS DECLARED (`@Head()`
|
|
13
|
+
reports HEAD even though Hono serves it under GET), fully composed path
|
|
14
|
+
(global prefix + version segment + controller prefix + route path),
|
|
15
|
+
controller name, handler name, version. Contributed routes
|
|
16
|
+
(`RouteContributor`/CRUD) mount directly on Hono and are observable via
|
|
17
|
+
`getHonoApp().routes`.
|
|
18
|
+
- **`app.getGlobalPrefix()`** — the prefix in effect ('' when none); also
|
|
19
|
+
what `openapi` tooling threads into `createOpenApiDocument`.
|
|
20
|
+
- **`Container.getModuleDescriptions(): ModuleDescription[]`** — the loaded
|
|
21
|
+
module graph (moduleId, imports, isGlobal, lazy, provider/export token
|
|
22
|
+
labels), load order plus the `__root__` bucket; reads registration state
|
|
23
|
+
only — safe pre/post bootstrap, never constructs, never claims lazy
|
|
24
|
+
modules.
|
|
25
|
+
- **`describeToken(token)`** — the token-label helper vela's own errors use,
|
|
26
|
+
exported for tooling.
|
|
27
|
+
|
|
3
28
|
## 1.14.0 (2026-07-04)
|
|
4
29
|
|
|
5
30
|
First-party `QueueModule` (roadmap phase 3, "the openness proof"): a whole
|
package/dist/application.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Hono } from 'hono';
|
|
|
2
2
|
import type { Container } from './container/container';
|
|
3
3
|
import type { Token } from './container/types';
|
|
4
4
|
import { EntrypointRegistry } from './entrypoint/entrypoint.registry';
|
|
5
|
-
import type { RouteManager } from './http/route.manager';
|
|
5
|
+
import type { RouteDescription, RouteManager } from './http/route.manager';
|
|
6
6
|
import type { MountOpenApiOptions } from './openapi/types';
|
|
7
7
|
import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
|
|
8
8
|
export declare class VelaApplication {
|
|
@@ -25,6 +25,15 @@ export declare class VelaApplication {
|
|
|
25
25
|
setInstances(instances: unknown[]): void;
|
|
26
26
|
get<T>(token: Token<T>): T;
|
|
27
27
|
getHonoApp(): Hono;
|
|
28
|
+
/**
|
|
29
|
+
* Every explicit controller route as the framework registered it (fully
|
|
30
|
+
* composed paths — the `vela route list` seam). Requires built routes;
|
|
31
|
+
* contributed routes (`@velajs/crud`) mount directly on Hono and are
|
|
32
|
+
* observable via `getHonoApp().routes` instead.
|
|
33
|
+
*/
|
|
34
|
+
describeRoutes(): RouteDescription[];
|
|
35
|
+
/** The global route prefix in effect ('' when none). */
|
|
36
|
+
getGlobalPrefix(): string;
|
|
28
37
|
useGlobalPipes(...pipes: PipeType[]): this;
|
|
29
38
|
useGlobalGuards(...guards: GuardType[]): this;
|
|
30
39
|
useGlobalInterceptors(...interceptors: InterceptorType[]): this;
|
package/dist/application.js
CHANGED
|
@@ -63,6 +63,18 @@ export class VelaApplication {
|
|
|
63
63
|
getHonoApp() {
|
|
64
64
|
return this.getApp();
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Every explicit controller route as the framework registered it (fully
|
|
68
|
+
* composed paths — the `vela route list` seam). Requires built routes;
|
|
69
|
+
* contributed routes (`@velajs/crud`) mount directly on Hono and are
|
|
70
|
+
* observable via `getHonoApp().routes` instead.
|
|
71
|
+
*/ describeRoutes() {
|
|
72
|
+
this.getApp(); // same routes-not-built guard as every HTTP accessor
|
|
73
|
+
return this.routeManager.getRouteDescriptions();
|
|
74
|
+
}
|
|
75
|
+
/** The global route prefix in effect ('' when none). */ getGlobalPrefix() {
|
|
76
|
+
return this.routeManager.getGlobalPrefix();
|
|
77
|
+
}
|
|
66
78
|
// Pipeline components — applied at request time, no rebuild needed
|
|
67
79
|
useGlobalPipes(...pipes) {
|
|
68
80
|
this.routeManager.useGlobalPipes(...pipes);
|
|
@@ -4,7 +4,14 @@ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<
|
|
|
4
4
|
isGlobal?: boolean;
|
|
5
5
|
}>;
|
|
6
6
|
export declare class ConfigModule extends ConfigurableModuleClass {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Preserve (a) per-call generic inference over the flat config record and
|
|
9
|
+
* (b) the eager-validation contract: `validate` runs at call time so bad
|
|
10
|
+
* config fails fast, then is stripped so the derived `CONFIG_OPTIONS`
|
|
11
|
+
* provider is a passthrough (no double validation). `validateSchema` (the
|
|
12
|
+
* merged-config schema) is deferred to first read — its input needs env.
|
|
13
|
+
*/
|
|
14
|
+
static forRoot<T extends Record<string, unknown>>(options?: ConfigModuleOptions<T> & {
|
|
8
15
|
isGlobal?: boolean;
|
|
9
16
|
key?: string;
|
|
10
17
|
}): DynamicModule;
|
|
@@ -1,51 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
}
|
|
7
|
-
import { Module } from "../module/decorators.js";
|
|
8
|
-
import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
|
|
1
|
+
import { Container } from "../container/container.js";
|
|
2
|
+
import { defineModule } from "../module/define-module.js";
|
|
9
3
|
import { ConfigService } from "./config.service.js";
|
|
4
|
+
import { ConfigStore } from "./config.store.js";
|
|
10
5
|
import { CONFIG_OPTIONS } from "./config.tokens.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Carrier for the namespace `asProvider()` KEY providers ONLY. Split out of
|
|
8
|
+
* ConfigModule and marked `lazy: true` so those factories — which inject
|
|
9
|
+
* `CONFIG_ENV`, live per-request on edge runtimes — never run at bootstrap.
|
|
10
|
+
* ConfigModule itself stays EAGER (see below).
|
|
11
|
+
*/ let ConfigNamespacesModule = class ConfigNamespacesModule {
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Build the lazy sub-module holding one KEY provider per namespace. Its `key`
|
|
15
|
+
* is derived from the owning ConfigModule instance key so multiple ConfigModule
|
|
16
|
+
* instances get distinct sub-module instances (no import-dedup collision).
|
|
17
|
+
*/ function namespacesSubModule(load, key) {
|
|
18
|
+
return {
|
|
19
|
+
module: ConfigNamespacesModule,
|
|
20
|
+
key: `config-ns:${key}`,
|
|
21
|
+
lazy: true,
|
|
22
|
+
providers: load.map((namespace)=>namespace.asProvider()),
|
|
23
|
+
exports: load.map((namespace)=>namespace.KEY)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Rebuilt on `defineModule` (the blessed engine — CLAUDE.md: changed modules
|
|
27
|
+
// use it). ConfigModule is EAGER: `ConfigService`, `ConfigStore`, and
|
|
28
|
+
// `CONFIG_OPTIONS` construct at bootstrap, so a `forRootAsync` async
|
|
29
|
+
// `useFactory` resolves in the awaited bootstrap pass and a *synchronous*
|
|
30
|
+
// `app.get(ConfigService)` afterwards returns the cached singleton (no drain).
|
|
31
|
+
//
|
|
32
|
+
// ONLY the namespace `asProvider()` KEY providers are deferred — they move into
|
|
33
|
+
// a LAZY sub-module (`ConfigNamespacesModule`) that ConfigModule imports and
|
|
34
|
+
// re-exports. Their factories inject `CONFIG_ENV` (live per-request on edge
|
|
35
|
+
// runtimes), so construction must not happen at bootstrap. `ConfigStore`
|
|
36
|
+
// resolves each KEY through the container on first `get()`; that first
|
|
37
|
+
// resolution claims + sync-drains the sub-module (namespace factories are
|
|
38
|
+
// synchronous, so the sync seam is safe). `resolveAllInstances` treats the KEY
|
|
39
|
+
// tokens as lazy-only (they belong solely to the lazy sub-module) and skips
|
|
40
|
+
// them at bootstrap.
|
|
41
|
+
const { ConfigurableModuleClass } = defineModule({
|
|
42
|
+
name: 'Config',
|
|
43
|
+
setup: ({ OPTIONS, options, key })=>{
|
|
44
|
+
const load = options.load ?? [];
|
|
45
|
+
const { validateSchema } = options;
|
|
46
|
+
return {
|
|
47
|
+
// Lazy sub-module carries the KEY providers; re-exported below so direct
|
|
48
|
+
// `@Inject(ns.KEY)` and `ConfigStore`'s container lookup both reach them.
|
|
49
|
+
imports: load.length > 0 ? [
|
|
50
|
+
namespacesSubModule(load, key)
|
|
51
|
+
] : [],
|
|
52
|
+
providers: [
|
|
53
|
+
{
|
|
54
|
+
// Flat config record. `validate` is applied here for the async path;
|
|
55
|
+
// `forRoot` pre-validates eagerly and strips it (see the override).
|
|
56
|
+
provide: CONFIG_OPTIONS,
|
|
57
|
+
useFactory: (opts)=>opts.validate ? opts.validate(opts.config ?? {}) : opts.config ?? {},
|
|
58
|
+
inject: [
|
|
59
|
+
OPTIONS
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// Factory-provided so the store closes over the namespace list +
|
|
64
|
+
// schema; it resolves each namespace's KEY lazily via the container.
|
|
65
|
+
provide: ConfigStore,
|
|
66
|
+
useFactory: (container, config)=>new ConfigStore(container, config, load, validateSchema),
|
|
67
|
+
inject: [
|
|
68
|
+
Container,
|
|
69
|
+
CONFIG_OPTIONS
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
ConfigService
|
|
73
|
+
],
|
|
74
|
+
exports: [
|
|
75
|
+
ConfigService,
|
|
76
|
+
ConfigStore,
|
|
77
|
+
CONFIG_OPTIONS,
|
|
78
|
+
...load.map((n)=>n.KEY)
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
19
83
|
export class ConfigModule extends ConfigurableModuleClass {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Preserve (a) per-call generic inference over the flat config record and
|
|
86
|
+
* (b) the eager-validation contract: `validate` runs at call time so bad
|
|
87
|
+
* config fails fast, then is stripped so the derived `CONFIG_OPTIONS`
|
|
88
|
+
* provider is a passthrough (no double validation). `validateSchema` (the
|
|
89
|
+
* merged-config schema) is deferred to first read — its input needs env.
|
|
90
|
+
*/ static forRoot(options = {}) {
|
|
91
|
+
const validatedConfig = options.validate ? options.validate(options.config ?? {}) : options.config;
|
|
27
92
|
const { validate: _validate, ...rest } = options;
|
|
28
93
|
return super.forRoot({
|
|
29
94
|
...rest,
|
|
30
|
-
config:
|
|
95
|
+
config: validatedConfig
|
|
31
96
|
});
|
|
32
97
|
}
|
|
33
98
|
}
|
|
34
|
-
ConfigModule = _ts_decorate([
|
|
35
|
-
Module({
|
|
36
|
-
providers: [
|
|
37
|
-
ConfigService,
|
|
38
|
-
{
|
|
39
|
-
provide: CONFIG_OPTIONS,
|
|
40
|
-
useFactory: (options)=>options.validate ? options.validate(options.config) : options.config,
|
|
41
|
-
inject: [
|
|
42
|
-
MODULE_OPTIONS_TOKEN
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
exports: [
|
|
47
|
-
ConfigService,
|
|
48
|
-
CONFIG_OPTIONS
|
|
49
|
-
]
|
|
50
|
-
})
|
|
51
|
-
], ConfigModule);
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import { ConfigStore } from './config.store';
|
|
2
|
+
import type { ConfigPath, ConfigPathValue } from './config.types';
|
|
3
|
+
/**
|
|
4
|
+
* Typed, dot-notation reads over the merged config. Stays a singleton (NestJS
|
|
5
|
+
* parity); delegates to the {@link ConfigStore}. Pass `ConfigType<[...]>` as the
|
|
6
|
+
* generic to type namespaced paths (`cfg.get('database.url')` → `string`).
|
|
7
|
+
*/
|
|
1
8
|
export declare class ConfigService<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
2
|
-
private readonly
|
|
3
|
-
constructor(
|
|
4
|
-
|
|
5
|
-
get<
|
|
9
|
+
private readonly store;
|
|
10
|
+
constructor(store: ConfigStore);
|
|
11
|
+
/** Read a config value; `undefined` when absent, or the supplied default. */
|
|
12
|
+
get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P> | undefined;
|
|
13
|
+
get<P extends ConfigPath<T>>(path: P, defaultValue: ConfigPathValue<T, P>): ConfigPathValue<T, P>;
|
|
14
|
+
/** Read a config value; throws when the path is absent. */
|
|
15
|
+
getOrThrow<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P>;
|
|
16
|
+
/** Whether a config path resolves to a defined value. */
|
|
17
|
+
has<P extends ConfigPath<T>>(path: P): boolean;
|
|
18
|
+
/** The full merged config object (flat record + resolved namespaces). */
|
|
6
19
|
getAll(): T;
|
|
7
20
|
}
|
|
@@ -12,34 +12,32 @@ function _ts_param(paramIndex, decorator) {
|
|
|
12
12
|
decorator(target, key, paramIndex);
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
import { Injectable } from "../container/decorators.js";
|
|
16
|
-
import {
|
|
17
|
-
import { CONFIG_OPTIONS } from "./config.tokens.js";
|
|
15
|
+
import { Injectable, Inject } from "../container/decorators.js";
|
|
16
|
+
import { ConfigStore } from "./config.store.js";
|
|
18
17
|
export class ConfigService {
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
this.
|
|
18
|
+
store;
|
|
19
|
+
constructor(store){
|
|
20
|
+
this.store = store;
|
|
22
21
|
}
|
|
23
|
-
get(
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
for (const part of parts){
|
|
27
|
-
if (current === null || current === undefined || typeof current !== 'object') {
|
|
28
|
-
return defaultValue;
|
|
29
|
-
}
|
|
30
|
-
current = current[part];
|
|
31
|
-
}
|
|
32
|
-
return current ?? defaultValue;
|
|
22
|
+
get(path, defaultValue) {
|
|
23
|
+
const value = this.store.get(path);
|
|
24
|
+
return value === undefined ? defaultValue : value;
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
return this.
|
|
26
|
+
/** Read a config value; throws when the path is absent. */ getOrThrow(path) {
|
|
27
|
+
return this.store.getOrThrow(path);
|
|
28
|
+
}
|
|
29
|
+
/** Whether a config path resolves to a defined value. */ has(path) {
|
|
30
|
+
return this.store.has(path);
|
|
31
|
+
}
|
|
32
|
+
/** The full merged config object (flat record + resolved namespaces). */ getAll() {
|
|
33
|
+
return this.store.all();
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
ConfigService = _ts_decorate([
|
|
39
37
|
Injectable(),
|
|
40
|
-
_ts_param(0, Inject(
|
|
38
|
+
_ts_param(0, Inject(ConfigStore)),
|
|
41
39
|
_ts_metadata("design:type", Function),
|
|
42
40
|
_ts_metadata("design:paramtypes", [
|
|
43
|
-
typeof
|
|
41
|
+
typeof ConfigStore === "undefined" ? Object : ConfigStore
|
|
44
42
|
])
|
|
45
43
|
], ConfigService);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Container } from '../container/container';
|
|
2
|
+
import type { ConfigSchema } from './config.types';
|
|
3
|
+
import type { AnyConfigNamespace } from './register-as';
|
|
4
|
+
/**
|
|
5
|
+
* Singleton source of truth for merged configuration.
|
|
6
|
+
*
|
|
7
|
+
* Holds the flat `config` record plus a `namespace → KEY` map. Namespaces are
|
|
8
|
+
* resolved LAZILY — the KEY is pulled from the container on the first read of
|
|
9
|
+
* that namespace and cached — so the store never forces namespace factories to
|
|
10
|
+
* materialize at construction. Resolution uses the synchronous container seam
|
|
11
|
+
* (`resolve`), which stays compatible with lazy modules.
|
|
12
|
+
*
|
|
13
|
+
* The merged shape is `{ ...config, [ns]: value }` — a namespace shadows a flat
|
|
14
|
+
* key of the same name.
|
|
15
|
+
*
|
|
16
|
+
* Constructed via a factory provider in {@link ConfigModule} so it can close
|
|
17
|
+
* over the `load` list and `validateSchema` without extra tokens.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ConfigStore {
|
|
20
|
+
private readonly container;
|
|
21
|
+
private readonly config;
|
|
22
|
+
private readonly validateSchema?;
|
|
23
|
+
private readonly namespaceKeys;
|
|
24
|
+
private readonly cache;
|
|
25
|
+
private validated;
|
|
26
|
+
private validationError;
|
|
27
|
+
constructor(container: Pick<Container, 'resolve'>, config?: Record<string, unknown>, namespaces?: ReadonlyArray<AnyConfigNamespace>, validateSchema?: ConfigSchema | undefined);
|
|
28
|
+
/** Read a value by dot-notation path; `undefined` if absent. */
|
|
29
|
+
get(path: string): unknown;
|
|
30
|
+
/** Read a value by dot-notation path; throws if absent. */
|
|
31
|
+
getOrThrow(path: string): unknown;
|
|
32
|
+
/** Whether a dot-notation path resolves to a defined value. */
|
|
33
|
+
has(path: string): boolean;
|
|
34
|
+
/** The full merged config object (flat record + resolved namespaces). */
|
|
35
|
+
all(): Record<string, unknown>;
|
|
36
|
+
private read;
|
|
37
|
+
/** Resolve a namespace's KEY through the container on first use, then cache. */
|
|
38
|
+
private resolveNamespace;
|
|
39
|
+
private merged;
|
|
40
|
+
/**
|
|
41
|
+
* Validate the merged config once, on first read. Deferred (not at bootstrap)
|
|
42
|
+
* because the merged shape needs namespace factories — and their env — which
|
|
43
|
+
* are only available after bootstrap on edge runtimes.
|
|
44
|
+
*/
|
|
45
|
+
private ensureValidated;
|
|
46
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
import { Injectable } from "../container/decorators.js";
|
|
11
|
+
/** Prototype-pollution guard for dot-notation segments. */ const DANGEROUS_KEYS = new Set([
|
|
12
|
+
'__proto__',
|
|
13
|
+
'constructor',
|
|
14
|
+
'prototype'
|
|
15
|
+
]);
|
|
16
|
+
function runSchema(schema, config) {
|
|
17
|
+
if (typeof schema === 'function') {
|
|
18
|
+
schema(config);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (schema && typeof schema.parse === 'function') {
|
|
22
|
+
schema.parse(config);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw new Error('ConfigModule `validateSchema` must be a function or expose a `parse()` method');
|
|
26
|
+
}
|
|
27
|
+
export class ConfigStore {
|
|
28
|
+
container;
|
|
29
|
+
config;
|
|
30
|
+
validateSchema;
|
|
31
|
+
namespaceKeys = new Map();
|
|
32
|
+
cache = new Map();
|
|
33
|
+
validated = false;
|
|
34
|
+
validationError;
|
|
35
|
+
constructor(container, config = {}, namespaces = [], validateSchema){
|
|
36
|
+
this.container = container;
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.validateSchema = validateSchema;
|
|
39
|
+
for (const ns of namespaces){
|
|
40
|
+
this.namespaceKeys.set(ns.namespace, ns.KEY);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Read a value by dot-notation path; `undefined` if absent. */ get(path) {
|
|
44
|
+
this.ensureValidated();
|
|
45
|
+
return this.read(path);
|
|
46
|
+
}
|
|
47
|
+
/** Read a value by dot-notation path; throws if absent. */ getOrThrow(path) {
|
|
48
|
+
const value = this.get(path);
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
throw new Error(`Configuration key "${path}" was not found`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Whether a dot-notation path resolves to a defined value. */ has(path) {
|
|
55
|
+
this.ensureValidated();
|
|
56
|
+
return this.read(path) !== undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The full merged config object (flat record + resolved namespaces). */ all() {
|
|
59
|
+
this.ensureValidated();
|
|
60
|
+
return this.merged();
|
|
61
|
+
}
|
|
62
|
+
read(path) {
|
|
63
|
+
const [head, ...rest] = path.split('.');
|
|
64
|
+
if (head === undefined || DANGEROUS_KEYS.has(head)) return undefined;
|
|
65
|
+
let current = this.namespaceKeys.has(head) ? this.resolveNamespace(head) : this.config[head];
|
|
66
|
+
for (const key of rest){
|
|
67
|
+
if (DANGEROUS_KEYS.has(key)) return undefined;
|
|
68
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
current = current[key];
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
/** Resolve a namespace's KEY through the container on first use, then cache. */ resolveNamespace(namespace) {
|
|
76
|
+
if (this.cache.has(namespace)) return this.cache.get(namespace);
|
|
77
|
+
const key = this.namespaceKeys.get(namespace);
|
|
78
|
+
const value = key === undefined ? undefined : this.container.resolve(key);
|
|
79
|
+
this.cache.set(namespace, value);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
merged() {
|
|
83
|
+
const out = {
|
|
84
|
+
...this.config
|
|
85
|
+
};
|
|
86
|
+
for (const namespace of this.namespaceKeys.keys()){
|
|
87
|
+
out[namespace] = this.resolveNamespace(namespace);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Validate the merged config once, on first read. Deferred (not at bootstrap)
|
|
93
|
+
* because the merged shape needs namespace factories — and their env — which
|
|
94
|
+
* are only available after bootstrap on edge runtimes.
|
|
95
|
+
*/ ensureValidated() {
|
|
96
|
+
if (this.validated) {
|
|
97
|
+
if (this.validationError) throw this.validationError; // fail-closed on every read
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.validated = true; // run the schema exactly once
|
|
101
|
+
if (!this.validateSchema) return;
|
|
102
|
+
try {
|
|
103
|
+
runSchema(this.validateSchema, this.merged());
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.validationError = err;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
ConfigStore = _ts_decorate([
|
|
111
|
+
Injectable(),
|
|
112
|
+
_ts_metadata("design:type", Function),
|
|
113
|
+
_ts_metadata("design:paramtypes", [
|
|
114
|
+
typeof Pick === "undefined" ? Object : Pick,
|
|
115
|
+
typeof Record === "undefined" ? Object : Record,
|
|
116
|
+
typeof ReadonlyArray === "undefined" ? Object : ReadonlyArray,
|
|
117
|
+
typeof ConfigSchema === "undefined" ? Object : ConfigSchema
|
|
118
|
+
])
|
|
119
|
+
], ConfigStore);
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import { InjectionToken } from '../container/types';
|
|
2
2
|
export declare const CONFIG_OPTIONS: InjectionToken<Record<string, unknown>>;
|
|
3
|
+
/**
|
|
4
|
+
* Ambient environment record consumed by config-namespace factories
|
|
5
|
+
* (`registerAs`). It is the multi-runtime substitute for stratal's
|
|
6
|
+
* Cloudflare-specific `env` token: core never touches `process.env`.
|
|
7
|
+
*
|
|
8
|
+
* The token self-provides `{}` by default (so factories resolve even with no
|
|
9
|
+
* platform binding), and stays OVERRIDABLE — a platform adapter
|
|
10
|
+
* (`@velajs/cloudflare`) or a future `./config-node` subpath provides a global
|
|
11
|
+
* `CONFIG_ENV` and that registration wins over the default.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CONFIG_ENV: InjectionToken<Record<string, unknown>>;
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import { InjectionToken } from "../container/types.js";
|
|
2
2
|
export const CONFIG_OPTIONS = new InjectionToken('CONFIG_OPTIONS');
|
|
3
|
+
/**
|
|
4
|
+
* Ambient environment record consumed by config-namespace factories
|
|
5
|
+
* (`registerAs`). It is the multi-runtime substitute for stratal's
|
|
6
|
+
* Cloudflare-specific `env` token: core never touches `process.env`.
|
|
7
|
+
*
|
|
8
|
+
* The token self-provides `{}` by default (so factories resolve even with no
|
|
9
|
+
* platform binding), and stays OVERRIDABLE — a platform adapter
|
|
10
|
+
* (`@velajs/cloudflare`) or a future `./config-node` subpath provides a global
|
|
11
|
+
* `CONFIG_ENV` and that registration wins over the default.
|
|
12
|
+
*/ export const CONFIG_ENV = new InjectionToken('CONFIG_ENV', {
|
|
13
|
+
factory: ()=>({})
|
|
14
|
+
});
|
|
@@ -1,5 +1,41 @@
|
|
|
1
|
+
import type { AnyConfigNamespace } from './register-as';
|
|
2
|
+
/**
|
|
3
|
+
* A schema for validating the fully-merged config. Kept structural so zod stays
|
|
4
|
+
* an OPTIONAL peer — a zod schema (has `.parse`) or a plain function both work;
|
|
5
|
+
* core never imports zod. Throwing rejects the config.
|
|
6
|
+
*/
|
|
7
|
+
export type ConfigSchema = ((config: Record<string, unknown>) => unknown) | {
|
|
8
|
+
parse: (config: unknown) => unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Options for {@link ConfigModule}.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: `load` and `validateSchema` are **`forRoot`-only** — they are read
|
|
14
|
+
* structurally from the call-time options bag. On the `forRootAsync` path the
|
|
15
|
+
* options come from a DI-resolved factory, so only the flat `config` record is
|
|
16
|
+
* supported there (async namespaces are out of scope; declare them via
|
|
17
|
+
* `forRoot`).
|
|
18
|
+
*/
|
|
1
19
|
export interface ConfigModuleOptions<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
2
|
-
config
|
|
20
|
+
/** Flat config record — the pre-namespace `config` path (still supported). */
|
|
21
|
+
config?: T;
|
|
22
|
+
/** Config namespaces created via `registerAs()`, merged under their namespace name. `forRoot`-only. */
|
|
23
|
+
load?: AnyConfigNamespace[];
|
|
24
|
+
/** Eager validator for the flat `config` record; runs at `forRoot()` call time. */
|
|
3
25
|
validate?: (config: T) => T;
|
|
26
|
+
/** Schema validating the MERGED config (flat + namespaces); runs lazily on first read. `forRoot`-only. */
|
|
27
|
+
validateSchema?: ConfigSchema;
|
|
4
28
|
isGlobal?: boolean;
|
|
5
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* All valid dot-notation paths of a config object type.
|
|
32
|
+
* @example `ConfigPath<{ database: { url: string } }>` → `'database' | 'database.url'`
|
|
33
|
+
*/
|
|
34
|
+
export type ConfigPath<T> = {
|
|
35
|
+
[K in keyof T & string]: T[K] extends Record<string, unknown> ? K | `${K}.${ConfigPath<T[K]>}` : K;
|
|
36
|
+
}[keyof T & string];
|
|
37
|
+
/**
|
|
38
|
+
* The value type at a dot-notation path.
|
|
39
|
+
* @example `ConfigPathValue<{ database: { url: string } }, 'database.url'>` → `string`
|
|
40
|
+
*/
|
|
41
|
+
export type ConfigPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends Record<string, unknown> ? ConfigPathValue<T[K], Rest> : never : never : P extends keyof T ? T[P] : never;
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { ConfigModule } from './config.module';
|
|
2
2
|
export { ConfigService } from './config.service';
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
3
|
+
export { ConfigStore } from './config.store';
|
|
4
|
+
export { CONFIG_OPTIONS, CONFIG_ENV } from './config.tokens';
|
|
5
|
+
export { registerAs } from './register-as';
|
|
6
|
+
export type { ConfigNamespace, AnyConfigNamespace, InferConfigType, ConfigType } from './register-as';
|
|
7
|
+
export type { ConfigModuleOptions, ConfigSchema, ConfigPath, ConfigPathValue } from './config.types';
|
package/dist/config/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { ConfigModule } from "./config.module.js";
|
|
2
2
|
export { ConfigService } from "./config.service.js";
|
|
3
|
-
export {
|
|
3
|
+
export { ConfigStore } from "./config.store.js";
|
|
4
|
+
export { CONFIG_OPTIONS, CONFIG_ENV } from "./config.tokens.js";
|
|
5
|
+
export { registerAs } from "./register-as.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InjectionToken, ProviderOptions } from '../container/types';
|
|
2
|
+
/**
|
|
3
|
+
* The result of {@link registerAs}: a namespaced config factory plus the DI
|
|
4
|
+
* token it is provided under.
|
|
5
|
+
*/
|
|
6
|
+
export interface ConfigNamespace<TKey extends string = string, TEnv = Record<string, unknown>, TConfig extends object = object> {
|
|
7
|
+
/** Injection token for this namespace's resolved config (`Symbol.for('vela:config:<ns>')`). */
|
|
8
|
+
readonly KEY: InjectionToken<TConfig>;
|
|
9
|
+
/** The namespace name (e.g. `'database'`). */
|
|
10
|
+
readonly namespace: TKey;
|
|
11
|
+
/** Factory receiving the ambient env and returning the namespace config. */
|
|
12
|
+
readonly factory: (env: TEnv) => TConfig;
|
|
13
|
+
/** Provider registration injecting {@link CONFIG_ENV} — spread into a module's providers. */
|
|
14
|
+
asProvider(): ProviderOptions<TConfig>;
|
|
15
|
+
}
|
|
16
|
+
/** Any config namespace — structural typing for the module `load` list. */
|
|
17
|
+
export type AnyConfigNamespace = ConfigNamespace<string, any, object>;
|
|
18
|
+
/**
|
|
19
|
+
* Create a namespaced configuration factory (NestJS `registerAs` parity).
|
|
20
|
+
*
|
|
21
|
+
* The `KEY` is minted with `Symbol.for` so it keeps a stable identity across
|
|
22
|
+
* module re-evaluation (HMR / repeated imports) — a fresh `InjectionToken`
|
|
23
|
+
* would mint a new identity each eval and break dedup. It is typed as
|
|
24
|
+
* `InjectionToken<TConfig>` purely for injection-site DX.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* export const dbConfig = registerAs('database', (env: Env) => ({
|
|
29
|
+
* url: env.DATABASE_URL,
|
|
30
|
+
* pool: 10,
|
|
31
|
+
* }));
|
|
32
|
+
* // ConfigModule.forRoot({ load: [dbConfig] });
|
|
33
|
+
* // cfg.get('database.url'); // via ConfigService
|
|
34
|
+
* // constructor(@Inject(dbConfig.KEY) db: InferConfigType<typeof dbConfig>) {}
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function registerAs<TKey extends string, TEnv, TConfig extends object>(namespace: TKey, factory: (env: TEnv) => TConfig): ConfigNamespace<TKey, TEnv, TConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Extract a namespace's config shape from a {@link registerAs} result.
|
|
40
|
+
*
|
|
41
|
+
* @example `type Db = InferConfigType<typeof dbConfig>` → `{ url: string; pool: number }`
|
|
42
|
+
*/
|
|
43
|
+
export type InferConfigType<T> = T extends ConfigNamespace<string, any, infer C> ? C : never;
|
|
44
|
+
/**
|
|
45
|
+
* Zero-codegen typed config shape from a tuple of namespaces — pass as the
|
|
46
|
+
* `ConfigService` generic to type dot-notation reads.
|
|
47
|
+
*
|
|
48
|
+
* @example `ConfigService<ConfigType<[typeof dbConfig, typeof mailConfig]>>`
|
|
49
|
+
*/
|
|
50
|
+
export type ConfigType<L extends readonly AnyConfigNamespace[]> = {
|
|
51
|
+
[N in L[number] as N['namespace']]: InferConfigType<N>;
|
|
52
|
+
};
|