@velajs/feature-flags 0.1.0 → 0.1.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/CHANGELOG.md +6 -0
- package/dist/feature-flags.service-CNOZ1DhZ.js +343 -0
- package/dist/feature-flags.service-CNOZ1DhZ.js.map +1 -0
- package/dist/index.d.ts +115 -12
- package/dist/index.js +114 -15
- package/dist/index.js.map +1 -0
- package/dist/memory.driver-C-5Y2b7_.d.ts +235 -0
- package/dist/testing/index.d.ts +11 -11
- package/dist/testing/index.js +28 -34
- package/dist/testing/index.js.map +1 -0
- package/package.json +53 -39
- package/dist/decorators/feature-flag.decorator.d.ts +0 -35
- package/dist/decorators/feature-flag.decorator.js +0 -26
- package/dist/drivers/driver.d.ts +0 -21
- package/dist/drivers/driver.js +0 -13
- package/dist/drivers/memory.driver.d.ts +0 -35
- package/dist/drivers/memory.driver.js +0 -50
- package/dist/drivers/registry.d.ts +0 -24
- package/dist/drivers/registry.js +0 -51
- package/dist/feature-flags.error.d.ts +0 -14
- package/dist/feature-flags.error.js +0 -15
- package/dist/feature-flags.module.d.ts +0 -21
- package/dist/feature-flags.module.js +0 -63
- package/dist/feature-flags.service.d.ts +0 -83
- package/dist/feature-flags.service.js +0 -208
- package/dist/feature-flags.tokens.d.ts +0 -16
- package/dist/feature-flags.tokens.js +0 -10
- package/dist/feature-flags.types.d.ts +0 -71
- package/dist/feature-flags.types.js +0 -3
- package/dist/guards/feature-flag.guard.d.ts +0 -24
- package/dist/guards/feature-flag.guard.js +0 -52
package/dist/index.js
CHANGED
|
@@ -1,15 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { a as FEATURE_FLAG_TOKENS, c as MemoryFlagDriver, i as __decorateMetadata, l as memoryFlagDriver, n as __decorate, o as FeatureFlagDriverRegistry, r as __decorateParam, s as buildDriverRegistry, t as FeatureFlagsService, u as FeatureFlagError } from "./feature-flags.service-CNOZ1DhZ.js";
|
|
2
|
+
import { APP_GUARD, ForbiddenException, Inject, Injectable, NotFoundException, REQUEST_CONTEXT, Reflector, Scope, SetMetadata, defineModule } from "@velajs/vela";
|
|
3
|
+
//#region src/decorators/feature-flag.decorator.ts
|
|
4
|
+
const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
|
|
5
|
+
/**
|
|
6
|
+
* Gate a route (handler) or controller behind a boolean feature flag. Pair
|
|
7
|
+
* with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
|
|
8
|
+
* app-wide registration).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* @UseGuards(FeatureFlagGuard)
|
|
13
|
+
* @Controller('/checkout')
|
|
14
|
+
* class CheckoutController {
|
|
15
|
+
* @FeatureFlag('new-checkout') // 404 when off
|
|
16
|
+
* @Get('/v2') v2() { ... }
|
|
17
|
+
*
|
|
18
|
+
* @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
19
|
+
* @Get('/beta') beta() { ... }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function FeatureFlag(key, options = {}) {
|
|
24
|
+
const meta = {
|
|
25
|
+
key,
|
|
26
|
+
onDisabled: options.onDisabled ?? "notFound"
|
|
27
|
+
};
|
|
28
|
+
return SetMetadata(FEATURE_FLAG_METADATA, meta);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/guards/feature-flag.guard.ts
|
|
32
|
+
let FeatureFlagGuard = class FeatureFlagGuard {
|
|
33
|
+
flags;
|
|
34
|
+
reflector = new Reflector();
|
|
35
|
+
constructor(flags) {
|
|
36
|
+
this.flags = flags;
|
|
37
|
+
}
|
|
38
|
+
async canActivate(context) {
|
|
39
|
+
const meta = this.reflector.getAllAndOverride(FEATURE_FLAG_METADATA, context);
|
|
40
|
+
if (!meta) return true;
|
|
41
|
+
const requestContext = this.requestContext(context);
|
|
42
|
+
if (await (requestContext ? this.flags.forRequest(requestContext) : this.flags).getBooleanValue(meta.key)) return true;
|
|
43
|
+
if (meta.onDisabled === "forbidden") throw new ForbiddenException(`Feature "${meta.key}" is not enabled.`);
|
|
44
|
+
throw new NotFoundException();
|
|
45
|
+
}
|
|
46
|
+
/** The current request context, via the child container on the Hono context. */
|
|
47
|
+
requestContext(context) {
|
|
48
|
+
try {
|
|
49
|
+
return context.getContext().get("container")?.resolve(REQUEST_CONTEXT);
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
FeatureFlagGuard = __decorate([
|
|
56
|
+
Injectable(),
|
|
57
|
+
__decorateParam(0, Inject(FEATURE_FLAG_TOKENS.Service)),
|
|
58
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
59
|
+
], FeatureFlagGuard);
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/feature-flags.module.ts
|
|
62
|
+
/**
|
|
63
|
+
* The feature-flags module. Authored on vela's public `defineModule`, so
|
|
64
|
+
* `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
|
|
65
|
+
* matching `forRootAsync({ inject, useFactory, ... })` come for free.
|
|
66
|
+
*
|
|
67
|
+
* `lazy: true` is valid here: every provider is sync-constructible (a factory
|
|
68
|
+
* for the driver registry, a sync-constructor service, a guard) and there are
|
|
69
|
+
* no async lifecycle hooks — so the module defers to first use without
|
|
70
|
+
* violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
|
|
71
|
+
*
|
|
72
|
+
* `isGlobal: true` makes the module globally visible AND registers
|
|
73
|
+
* {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
|
|
74
|
+
* route is gated without a per-controller `@UseGuards`.
|
|
75
|
+
*/
|
|
76
|
+
const { ConfigurableModuleClass } = defineModule({
|
|
77
|
+
name: "FeatureFlags",
|
|
78
|
+
optionsToken: FEATURE_FLAG_TOKENS.Options,
|
|
79
|
+
lazy: true,
|
|
80
|
+
transform: (definition, extras) => extras.isGlobal ? {
|
|
81
|
+
...definition,
|
|
82
|
+
global: true,
|
|
83
|
+
providers: [...definition.providers ?? [], {
|
|
84
|
+
provide: APP_GUARD,
|
|
85
|
+
useExisting: FeatureFlagGuard
|
|
86
|
+
}]
|
|
87
|
+
} : definition,
|
|
88
|
+
setup: ({ OPTIONS }) => ({
|
|
89
|
+
providers: [
|
|
90
|
+
{
|
|
91
|
+
provide: FEATURE_FLAG_TOKENS.DriverRegistry,
|
|
92
|
+
useFactory: (options) => buildDriverRegistry(options),
|
|
93
|
+
inject: [OPTIONS]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
provide: FEATURE_FLAG_TOKENS.Service,
|
|
97
|
+
useClass: FeatureFlagsService,
|
|
98
|
+
scope: Scope.TRANSIENT
|
|
99
|
+
},
|
|
100
|
+
FeatureFlagGuard
|
|
101
|
+
],
|
|
102
|
+
exports: [
|
|
103
|
+
FEATURE_FLAG_TOKENS.Service,
|
|
104
|
+
FEATURE_FLAG_TOKENS.DriverRegistry,
|
|
105
|
+
FeatureFlagGuard,
|
|
106
|
+
OPTIONS
|
|
107
|
+
]
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
var FeatureFlagsModule = class extends ConfigurableModuleClass {};
|
|
111
|
+
//#endregion
|
|
112
|
+
export { FEATURE_FLAG_METADATA, FEATURE_FLAG_TOKENS, FeatureFlag, FeatureFlagDriverRegistry, FeatureFlagError, FeatureFlagGuard, FeatureFlagsModule, FeatureFlagsService, MemoryFlagDriver, buildDriverRegistry, memoryFlagDriver };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/decorators/feature-flag.decorator.ts","../src/guards/feature-flag.guard.ts","../src/feature-flags.module.ts"],"sourcesContent":["import { SetMetadata } from '@velajs/vela';\nimport type { FlagKey } from '../feature-flags.types';\n\n/** What the {@link FeatureFlagGuard} does when a gated flag is off. */\nexport type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';\n\nexport interface FeatureFlagOptions {\n /**\n * Response when the flag is off. `'notFound'` (default) hides the route\n * entirely (404); `'forbidden'` reveals it exists but denies access (403).\n */\n onDisabled?: FeatureFlagDisabledBehavior;\n}\n\n/** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */\nexport interface FeatureFlagMetadata {\n key: string;\n onDisabled: FeatureFlagDisabledBehavior;\n}\n\nexport const FEATURE_FLAG_METADATA = 'vela:feature-flags:flag';\n\n/**\n * Gate a route (handler) or controller behind a boolean feature flag. Pair\n * with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`\n * app-wide registration).\n *\n * @example\n * ```ts\n * @UseGuards(FeatureFlagGuard)\n * @Controller('/checkout')\n * class CheckoutController {\n * @FeatureFlag('new-checkout') // 404 when off\n * @Get('/v2') v2() { ... }\n *\n * @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off\n * @Get('/beta') beta() { ... }\n * }\n * ```\n */\nexport function FeatureFlag(key: FlagKey, options: FeatureFlagOptions = {}) {\n const meta: FeatureFlagMetadata = { key, onDisabled: options.onDisabled ?? 'notFound' };\n return SetMetadata(FEATURE_FLAG_METADATA, meta);\n}\n","import {\n Container,\n ForbiddenException,\n Inject,\n Injectable,\n NotFoundException,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport {\n FEATURE_FLAG_METADATA,\n type FeatureFlagMetadata,\n} from '../decorators/feature-flag.decorator';\nimport type { FeatureFlagsService } from '../feature-flags.service';\nimport { FEATURE_FLAG_TOKENS } from '../feature-flags.tokens';\n\n/**\n * Route gate for `@FeatureFlag()`. Reads the handler/controller metadata, then\n * `getBooleanValue(key)`; when the flag is off it throws `NotFoundException`\n * (the route appears hidden) or `ForbiddenException` per the decorator option.\n * Handlers with no `@FeatureFlag()` metadata pass through untouched, so the\n * guard is safe to register app-wide (`FeatureFlagsModule.forRoot({ isGlobal:\n * true })`) or per-route via `@UseGuards(FeatureFlagGuard)`.\n *\n * It does NOT inject `REQUEST_CONTEXT` — that would make the guard request-\n * scoped and break lazy-module materialization (which constructs every provider\n * once, at first use, possibly outside a request). Instead it reads the request\n * context off the per-request child container carried on the Hono context, so\n * the module's `context` resolver still runs for the gate decision.\n */\n@Injectable()\nexport class FeatureFlagGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n constructor(@Inject(FEATURE_FLAG_TOKENS.Service) private readonly flags: FeatureFlagsService) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const meta = this.reflector.getAllAndOverride<FeatureFlagMetadata>(\n FEATURE_FLAG_METADATA,\n context,\n );\n if (!meta) return true;\n\n const requestContext = this.requestContext(context);\n const flags = requestContext ? this.flags.forRequest(requestContext) : this.flags;\n const enabled = await flags.getBooleanValue(meta.key);\n if (enabled) return true;\n\n if (meta.onDisabled === 'forbidden') {\n throw new ForbiddenException(`Feature \"${meta.key}\" is not enabled.`);\n }\n throw new NotFoundException();\n }\n\n /** The current request context, via the child container on the Hono context. */\n private requestContext(context: ExecutionContext): RequestContext | undefined {\n try {\n const container = context.getContext().get('container') as Container | undefined;\n return container?.resolve(REQUEST_CONTEXT);\n } catch {\n return undefined;\n }\n }\n}\n","import { APP_GUARD, Scope, defineModule } from '@velajs/vela';\nimport { buildDriverRegistry } from './drivers/registry';\nimport { FeatureFlagGuard } from './guards/feature-flag.guard';\nimport { FeatureFlagsService } from './feature-flags.service';\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens';\nimport type { FeatureFlagsOptions } from './feature-flags.types';\n\n/**\n * The feature-flags module. Authored on vela's public `defineModule`, so\n * `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the\n * matching `forRootAsync({ inject, useFactory, ... })` come for free.\n *\n * `lazy: true` is valid here: every provider is sync-constructible (a factory\n * for the driver registry, a sync-constructor service, a guard) and there are\n * no async lifecycle hooks — so the module defers to first use without\n * violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).\n *\n * `isGlobal: true` makes the module globally visible AND registers\n * {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`\n * route is gated without a per-controller `@UseGuards`.\n */\nconst { ConfigurableModuleClass } = defineModule<FeatureFlagsOptions>({\n name: 'FeatureFlags',\n optionsToken: FEATURE_FLAG_TOKENS.Options,\n lazy: true,\n transform: (definition, extras) =>\n (extras as { isGlobal?: boolean }).isGlobal\n ? {\n ...definition,\n global: true,\n providers: [\n ...(definition.providers ?? []),\n { provide: APP_GUARD, useExisting: FeatureFlagGuard },\n ],\n }\n : definition,\n setup: ({ OPTIONS }) => ({\n providers: [\n {\n provide: FEATURE_FLAG_TOKENS.DriverRegistry,\n useFactory: (options: FeatureFlagsOptions) => buildDriverRegistry(options),\n inject: [OPTIONS],\n },\n // Transient: a fresh service per injection, so `use()`/`forRequest()`\n // clones and per-request context stay isolated. The service never injects\n // REQUEST_CONTEXT, so it does not bubble to request scope and remains\n // resolvable in queue / scheduled / global scope.\n {\n provide: FEATURE_FLAG_TOKENS.Service,\n useClass: FeatureFlagsService,\n scope: Scope.TRANSIENT,\n },\n FeatureFlagGuard,\n ],\n exports: [\n FEATURE_FLAG_TOKENS.Service,\n FEATURE_FLAG_TOKENS.DriverRegistry,\n FeatureFlagGuard,\n OPTIONS,\n ],\n }),\n});\n\nexport class FeatureFlagsModule extends ConfigurableModuleClass {}\n"],"mappings":";;;AAoBA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;AAoBrC,SAAgB,YAAY,KAAc,UAA8B,CAAC,GAAG;CAC1E,MAAM,OAA4B;EAAE;EAAK,YAAY,QAAQ,cAAc;CAAW;CACtF,OAAO,YAAY,uBAAuB,IAAI;AAChD;;;ACTO,IAAA,mBAAA,MAAM,iBAAwC;CAGe;CAFlE,YAA6B,IAAI,UAAU;CAE3C,YAAY,OAAkF;EAA5B,KAAA,QAAA;CAA6B;CAE/F,MAAM,YAAY,SAA6C;EAC7D,MAAM,OAAO,KAAK,UAAU,kBAC1B,uBACA,OACF;EACA,IAAI,CAAC,MAAM,OAAO;EAElB,MAAM,iBAAiB,KAAK,eAAe,OAAO;EAGlD,IAAI,OAFU,iBAAiB,KAAK,MAAM,WAAW,cAAc,IAAI,KAAK,MAAA,CAChD,gBAAgB,KAAK,GAAG,GACvC,OAAO;EAEpB,IAAI,KAAK,eAAe,aACtB,MAAM,IAAI,mBAAmB,YAAY,KAAK,IAAI,kBAAkB;EAEtE,MAAM,IAAI,kBAAkB;CAC9B;;CAGA,eAAuB,SAAuD;EAC5E,IAAI;GAEF,OADkB,QAAQ,WAAW,CAAC,CAAC,IAAI,WAC5B,CAAC,EAAE,QAAQ,eAAe;EAC3C,QAAQ;GACN;EACF;CACF;AACF;;CAjCC,WAAW;oBAIG,OAAO,oBAAoB,OAAO,CAAA;;;;;;;;;;;;;;;;;;;AChBjD,MAAM,EAAE,4BAA4B,aAAkC;CACpE,MAAM;CACN,cAAc,oBAAoB;CAClC,MAAM;CACN,YAAY,YAAY,WACrB,OAAkC,WAC/B;EACE,GAAG;EACH,QAAQ;EACR,WAAW,CACT,GAAI,WAAW,aAAa,CAAC,GAC7B;GAAE,SAAS;GAAW,aAAa;EAAiB,CACtD;CACF,IACA;CACN,QAAQ,EAAE,eAAe;EACvB,WAAW;GACT;IACE,SAAS,oBAAoB;IAC7B,aAAa,YAAiC,oBAAoB,OAAO;IACzE,QAAQ,CAAC,OAAO;GAClB;GAKA;IACE,SAAS,oBAAoB;IAC7B,UAAU;IACV,OAAO,MAAM;GACf;GACA;EACF;EACA,SAAS;GACP,oBAAoB;GACpB,oBAAoB;GACpB;GACA;EACF;CACF;AACF,CAAC;AAED,IAAa,qBAAb,cAAwC,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { LoggerService, RequestContext } from "@velajs/vela";
|
|
2
|
+
//#region src/drivers/driver.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The cross-package contract every feature-flag backend implements.
|
|
5
|
+
*
|
|
6
|
+
* This is the seam that platform packages plug into: `@velajs/cloudflare`
|
|
7
|
+
* ships a Flagship-binding driver and a KV-backed driver against this exact
|
|
8
|
+
* interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
|
|
9
|
+
* maps a key + fallback (+ optional targeting context) onto a typed value and
|
|
10
|
+
* MUST return the `fallback` — never throw — when it cannot resolve the key.
|
|
11
|
+
*
|
|
12
|
+
* Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
|
|
13
|
+
* `FeatureFlagsService` around these four value methods; drivers may
|
|
14
|
+
* optionally override that synthesis, but the value methods are the contract.
|
|
15
|
+
*/
|
|
16
|
+
interface FeatureFlagDriver {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
|
|
19
|
+
getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
|
|
20
|
+
getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
|
|
21
|
+
getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/feature-flags.types.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* A value a feature flag can resolve to. Mirrors the four evaluation methods
|
|
27
|
+
* on {@link FeatureFlagDriver}.
|
|
28
|
+
*/
|
|
29
|
+
type FlagValue = boolean | string | number | object;
|
|
30
|
+
/**
|
|
31
|
+
* A free-form evaluation context handed to a driver (for targeting: user id,
|
|
32
|
+
* plan, country, …). Drivers that don't do targeting ignore it.
|
|
33
|
+
*/
|
|
34
|
+
type FlagContext = Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Augment this interface to get typed flag keys on the service and the
|
|
37
|
+
* `@FeatureFlag()` decorator.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* declare module '@velajs/feature-flags' {
|
|
42
|
+
* interface FeatureFlagRegistry {
|
|
43
|
+
* 'new-checkout': boolean;
|
|
44
|
+
* 'checkout-flow': string;
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
interface FeatureFlagRegistry {}
|
|
50
|
+
/**
|
|
51
|
+
* A flag key. Narrows to the declared {@link FeatureFlagRegistry} keys when the
|
|
52
|
+
* app augments it; falls back to `string` otherwise.
|
|
53
|
+
*/
|
|
54
|
+
type FlagKey = keyof FeatureFlagRegistry extends never ? string : keyof FeatureFlagRegistry & string;
|
|
55
|
+
/**
|
|
56
|
+
* A declared set of flags and their default values.
|
|
57
|
+
*
|
|
58
|
+
* Drivers have no enumeration API, so the flags you intend to evaluate as a
|
|
59
|
+
* batch (via {@link FeatureFlagsService.all}) must be declared once here. Each
|
|
60
|
+
* default also doubles as the type hint used to pick the evaluation method.
|
|
61
|
+
*/
|
|
62
|
+
type FlagManifest = Record<string, FlagValue>;
|
|
63
|
+
/** Why an evaluation returned the value it did. */
|
|
64
|
+
type FlagEvaluationReason = 'STATIC' | 'DEFAULT' | 'ERROR';
|
|
65
|
+
/** A flag value plus the metadata the service synthesizes around a driver read. */
|
|
66
|
+
interface FlagEvaluationDetails<T extends FlagValue = FlagValue> {
|
|
67
|
+
flagKey: string;
|
|
68
|
+
value: T;
|
|
69
|
+
reason: FlagEvaluationReason;
|
|
70
|
+
errorMessage?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Feature-flags module configuration.
|
|
74
|
+
*/
|
|
75
|
+
interface FeatureFlagsOptions {
|
|
76
|
+
/**
|
|
77
|
+
* The drivers this app can evaluate against. When omitted, a single
|
|
78
|
+
* in-memory {@link MemoryFlagDriver} named `"memory"` is used (all flags
|
|
79
|
+
* resolve to their declared defaults).
|
|
80
|
+
*/
|
|
81
|
+
drivers?: FeatureFlagDriver[];
|
|
82
|
+
/** Name of the driver the injected service targets. Defaults to `drivers[0].name`. */
|
|
83
|
+
default?: string;
|
|
84
|
+
/** Declared flags + defaults. Powers manifest defaults and {@link FeatureFlagsService.all}. */
|
|
85
|
+
manifest?: FlagManifest;
|
|
86
|
+
/**
|
|
87
|
+
* Resolves a per-request evaluation context (for example `{ userId }`) merged
|
|
88
|
+
* into every evaluation. Per-call context passed to a method overrides these.
|
|
89
|
+
* Receives the current request context; skipped outside request scope.
|
|
90
|
+
*/
|
|
91
|
+
context?: (ctx: RequestContext) => FlagContext | Promise<FlagContext>;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/drivers/registry.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* The set of registered {@link FeatureFlagDriver}s plus the default selection.
|
|
97
|
+
* Built once per module instance ({@link buildDriverRegistry}) and injected
|
|
98
|
+
* into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
|
|
99
|
+
*/
|
|
100
|
+
declare class FeatureFlagDriverRegistry {
|
|
101
|
+
private readonly drivers;
|
|
102
|
+
readonly defaultName: string;
|
|
103
|
+
constructor(drivers: readonly FeatureFlagDriver[], defaultName?: string);
|
|
104
|
+
/** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */
|
|
105
|
+
get(name: string): FeatureFlagDriver;
|
|
106
|
+
/** Resolve a named driver, or the default when `name` is omitted. */
|
|
107
|
+
resolve(name?: string): FeatureFlagDriver;
|
|
108
|
+
/** All registered driver names. */
|
|
109
|
+
names(): string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the driver registry from module options. Falls back to a single
|
|
113
|
+
* in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
|
|
114
|
+
* yields a working (all-defaults) flags service for local development.
|
|
115
|
+
*/
|
|
116
|
+
declare function buildDriverRegistry(options: FeatureFlagsOptions): FeatureFlagDriverRegistry;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/feature-flags.service.d.ts
|
|
119
|
+
/**
|
|
120
|
+
* The injectable consumers reach for. A thin, type-safe, never-throw wrapper
|
|
121
|
+
* over a {@link FeatureFlagDriver}, with two ergonomic additions:
|
|
122
|
+
*
|
|
123
|
+
* - **Manifest defaults** — omit a default and the value declared in the app's
|
|
124
|
+
* `manifest` is used (an explicit argument always wins).
|
|
125
|
+
* - **Default context** — the module's `context` resolver is merged into every
|
|
126
|
+
* evaluation (per-call context overrides it), resolved from the current
|
|
127
|
+
* request and skipped automatically outside request scope.
|
|
128
|
+
*
|
|
129
|
+
* Switch drivers with {@link use}; bind a request with {@link forRequest} (the
|
|
130
|
+
* guard does this). Evaluation NEVER throws — the driver returns the fallback
|
|
131
|
+
* on evaluation errors, and the service additionally absorbs any thrown error
|
|
132
|
+
* into the same fallback with a logged warning.
|
|
133
|
+
*
|
|
134
|
+
* `@Transient`: constructed synchronously (lazy-module-safe), a fresh instance
|
|
135
|
+
* per injection, and resolvable in and out of request scope — it never
|
|
136
|
+
* DI-injects `REQUEST_CONTEXT`, so it does not bubble to request scope.
|
|
137
|
+
*/
|
|
138
|
+
declare class FeatureFlagsService {
|
|
139
|
+
private readonly registry;
|
|
140
|
+
private readonly options;
|
|
141
|
+
private readonly boundContext?;
|
|
142
|
+
private readonly logger;
|
|
143
|
+
private readonly manifest;
|
|
144
|
+
private readonly driver;
|
|
145
|
+
constructor(registry: FeatureFlagDriverRegistry, options: FeatureFlagsOptions, logger?: LoggerService, boundContext?: RequestContext | undefined, boundDriver?: FeatureFlagDriver);
|
|
146
|
+
/** The name of the driver this instance targets. */
|
|
147
|
+
get driverName(): string;
|
|
148
|
+
/**
|
|
149
|
+
* Switch to a different registered driver. Returns a NEW immutable instance
|
|
150
|
+
* bound to `name`; the original is unchanged. Throws if `name` is unknown.
|
|
151
|
+
*/
|
|
152
|
+
use(name: string): FeatureFlagsService;
|
|
153
|
+
/**
|
|
154
|
+
* Bind a request context. Returns a NEW immutable instance whose evaluations
|
|
155
|
+
* merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
|
|
156
|
+
* resolved in request scope can also call it explicitly.
|
|
157
|
+
*/
|
|
158
|
+
forRequest(ctx: RequestContext): FeatureFlagsService;
|
|
159
|
+
/** Evaluate a flag as a `boolean`. */
|
|
160
|
+
getBooleanValue(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<boolean>;
|
|
161
|
+
/** Evaluate a flag as a `string`. */
|
|
162
|
+
getStringValue(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<string>;
|
|
163
|
+
/** Evaluate a flag as a `number`. */
|
|
164
|
+
getNumberValue(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<number>;
|
|
165
|
+
/** Evaluate a flag as a typed object. */
|
|
166
|
+
getObjectValue<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<T>;
|
|
167
|
+
/** Evaluate a `boolean` flag with synthesized evaluation metadata. */
|
|
168
|
+
getBooleanDetails(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<FlagEvaluationDetails<boolean>>;
|
|
169
|
+
/** Evaluate a `string` flag with synthesized evaluation metadata. */
|
|
170
|
+
getStringDetails(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<FlagEvaluationDetails<string>>;
|
|
171
|
+
/** Evaluate a `number` flag with synthesized evaluation metadata. */
|
|
172
|
+
getNumberDetails(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<FlagEvaluationDetails<number>>;
|
|
173
|
+
/** Evaluate a typed object flag with synthesized evaluation metadata. */
|
|
174
|
+
getObjectDetails<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<FlagEvaluationDetails<T>>;
|
|
175
|
+
/**
|
|
176
|
+
* Evaluate every flag declared in the manifest and return a `{ key: value }`
|
|
177
|
+
* map. The evaluation method is chosen from each declared default's type.
|
|
178
|
+
* A throwing context resolver falls back to the manifest defaults rather than
|
|
179
|
+
* taking the batch down.
|
|
180
|
+
*/
|
|
181
|
+
all(context?: FlagContext): Promise<Record<string, FlagValue>>;
|
|
182
|
+
/** Immutable clone with a different driver and/or bound context. */
|
|
183
|
+
private clone;
|
|
184
|
+
/** Resolve the merged evaluation context (default context + per-call override). */
|
|
185
|
+
private context;
|
|
186
|
+
/** Pick the default: explicit arg, then manifest, then the type's zero value. */
|
|
187
|
+
private fallback;
|
|
188
|
+
/** Evaluate a single flag, choosing the method from the declared default's type. */
|
|
189
|
+
private evaluate;
|
|
190
|
+
/**
|
|
191
|
+
* Run an evaluation and absorb any failure into the fallback. A flag lookup
|
|
192
|
+
* must never take the caller down with it.
|
|
193
|
+
*/
|
|
194
|
+
private safe;
|
|
195
|
+
private details;
|
|
196
|
+
private errorDetails;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/drivers/memory.driver.d.ts
|
|
200
|
+
interface MemoryFlagDriverOptions {
|
|
201
|
+
/** Driver name used for `use(name)` / the default-driver selection. Default `"memory"`. */
|
|
202
|
+
name?: string;
|
|
203
|
+
/** Initial flag values. */
|
|
204
|
+
values?: FlagManifest;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
|
|
208
|
+
* and the test fake in one. Reads from an internal `Map` seeded from options
|
|
209
|
+
* or mutated with {@link set}/{@link reset} — the memory driver ignores the
|
|
210
|
+
* evaluation context (it does no targeting). An unknown key returns the
|
|
211
|
+
* caller's `fallback`, exactly like a remote driver that can't resolve it.
|
|
212
|
+
*/
|
|
213
|
+
declare class MemoryFlagDriver implements FeatureFlagDriver {
|
|
214
|
+
readonly name: string;
|
|
215
|
+
private readonly store;
|
|
216
|
+
constructor(options?: MemoryFlagDriverOptions);
|
|
217
|
+
/** Set a flag value. Chainable. */
|
|
218
|
+
set(key: string, value: FlagValue): this;
|
|
219
|
+
/** Remove a flag (subsequent reads return the caller's fallback). Chainable. */
|
|
220
|
+
delete(key: string): this;
|
|
221
|
+
/** True when `key` has a stored value. */
|
|
222
|
+
has(key: string): boolean;
|
|
223
|
+
/** Clear all flags, then optionally seed a fresh set. Chainable. */
|
|
224
|
+
reset(values?: FlagManifest): this;
|
|
225
|
+
getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
|
|
226
|
+
getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
|
|
227
|
+
getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
|
|
228
|
+
getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
|
|
229
|
+
private read;
|
|
230
|
+
}
|
|
231
|
+
/** Convenience factory for {@link MemoryFlagDriver}. */
|
|
232
|
+
declare function memoryFlagDriver(options?: MemoryFlagDriverOptions): MemoryFlagDriver;
|
|
233
|
+
//#endregion
|
|
234
|
+
export { FeatureFlagDriverRegistry as a, FeatureFlagsOptions as c, FlagEvaluationReason as d, FlagKey as f, FeatureFlagDriver as h, FeatureFlagsService as i, FlagContext as l, FlagValue as m, MemoryFlagDriverOptions as n, buildDriverRegistry as o, FlagManifest as p, memoryFlagDriver as r, FeatureFlagRegistry as s, MemoryFlagDriver as t, FlagEvaluationDetails as u };
|
|
235
|
+
//# sourceMappingURL=memory.driver-C-5Y2b7_.d.ts.map
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { MemoryFlagDriver } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
service: FeatureFlagsService;
|
|
9
|
-
/** The backing memory driver — mutate flags with `.set()`/`.reset()`. */
|
|
10
|
-
driver: MemoryFlagDriver;
|
|
1
|
+
import { c as FeatureFlagsOptions, i as FeatureFlagsService, n as MemoryFlagDriverOptions, p as FlagManifest, r as memoryFlagDriver, t as MemoryFlagDriver } from "../memory.driver-C-5Y2b7_.js";
|
|
2
|
+
//#region src/testing/index.d.ts
|
|
3
|
+
interface TestFeatureFlags {
|
|
4
|
+
/** A real service bound to the memory driver below. */
|
|
5
|
+
service: FeatureFlagsService;
|
|
6
|
+
/** The backing memory driver — mutate flags with `.set()`/`.reset()`. */
|
|
7
|
+
driver: MemoryFlagDriver;
|
|
11
8
|
}
|
|
12
9
|
/**
|
|
13
10
|
* Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
|
|
@@ -22,4 +19,7 @@ export interface TestFeatureFlags {
|
|
|
22
19
|
* expect(await service.getBooleanValue('new-checkout')).toBe(false);
|
|
23
20
|
* ```
|
|
24
21
|
*/
|
|
25
|
-
|
|
22
|
+
declare function createTestFeatureFlags(values?: FlagManifest, options?: Pick<FeatureFlagsOptions, 'manifest' | 'context'>): TestFeatureFlags;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MemoryFlagDriver, type MemoryFlagDriverOptions, TestFeatureFlags, createTestFeatureFlags, memoryFlagDriver };
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/testing/index.js
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// FeatureFlagsService over an in-memory driver without bootstrapping a module.
|
|
4
|
-
import { FeatureFlagDriverRegistry } from "../drivers/registry.js";
|
|
5
|
-
import { MemoryFlagDriver } from "../drivers/memory.driver.js";
|
|
6
|
-
import { FeatureFlagsService } from "../feature-flags.service.js";
|
|
7
|
-
export { MemoryFlagDriver, memoryFlagDriver } from "../drivers/memory.driver.js";
|
|
1
|
+
import { c as MemoryFlagDriver, l as memoryFlagDriver, o as FeatureFlagDriverRegistry, t as FeatureFlagsService } from "../feature-flags.service-CNOZ1DhZ.js";
|
|
2
|
+
//#region src/testing/index.ts
|
|
8
3
|
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
return {
|
|
33
|
-
service,
|
|
34
|
-
driver
|
|
35
|
-
};
|
|
4
|
+
* Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
|
|
5
|
+
* app bootstrap required. Flip flags on the returned `driver` and assert on the
|
|
6
|
+
* `service`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
|
|
11
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(true);
|
|
12
|
+
* driver.set('new-checkout', false);
|
|
13
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(false);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function createTestFeatureFlags(values = {}, options = {}) {
|
|
17
|
+
const driver = new MemoryFlagDriver({ values });
|
|
18
|
+
return {
|
|
19
|
+
service: new FeatureFlagsService(new FeatureFlagDriverRegistry([driver], driver.name), {
|
|
20
|
+
default: driver.name,
|
|
21
|
+
manifest: options.manifest,
|
|
22
|
+
context: options.context
|
|
23
|
+
}),
|
|
24
|
+
driver
|
|
25
|
+
};
|
|
36
26
|
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { MemoryFlagDriver, createTestFeatureFlags, memoryFlagDriver };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/testing/index.ts"],"sourcesContent":["// @velajs/feature-flags/testing — the memory driver IS the fake. This subpath\n// re-exports it plus a zero-DI helper for unit tests that want a real\n// FeatureFlagsService over an in-memory driver without bootstrapping a module.\n\nimport { FeatureFlagDriverRegistry } from '../drivers/registry';\nimport { MemoryFlagDriver } from '../drivers/memory.driver';\nimport { FeatureFlagsService } from '../feature-flags.service';\nimport type { FeatureFlagsOptions, FlagManifest } from '../feature-flags.types';\n\nexport { MemoryFlagDriver, memoryFlagDriver } from '../drivers/memory.driver';\nexport type { MemoryFlagDriverOptions } from '../drivers/memory.driver';\n\nexport interface TestFeatureFlags {\n /** A real service bound to the memory driver below. */\n service: FeatureFlagsService;\n /** The backing memory driver — mutate flags with `.set()`/`.reset()`. */\n driver: MemoryFlagDriver;\n}\n\n/**\n * Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /\n * app bootstrap required. Flip flags on the returned `driver` and assert on the\n * `service`.\n *\n * @example\n * ```ts\n * const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });\n * expect(await service.getBooleanValue('new-checkout')).toBe(true);\n * driver.set('new-checkout', false);\n * expect(await service.getBooleanValue('new-checkout')).toBe(false);\n * ```\n */\nexport function createTestFeatureFlags(\n values: FlagManifest = {},\n options: Pick<FeatureFlagsOptions, 'manifest' | 'context'> = {},\n): TestFeatureFlags {\n const driver = new MemoryFlagDriver({ values });\n const registry = new FeatureFlagDriverRegistry([driver], driver.name);\n const service = new FeatureFlagsService(registry, {\n default: driver.name,\n manifest: options.manifest,\n context: options.context,\n });\n return { service, driver };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAgB,uBACd,SAAuB,CAAC,GACxB,UAA6D,CAAC,GAC5C;CAClB,MAAM,SAAS,IAAI,iBAAiB,EAAE,OAAO,CAAC;CAO9C,OAAO;EAAE,SAAA,IALW,oBAAoB,IADnB,0BAA0B,CAAC,MAAM,GAAG,OAAO,IACjB,GAAG;GAChD,SAAS,OAAO;GAChB,UAAU,QAAQ;GAClB,SAAS,QAAQ;EACnB,CACe;EAAG;CAAO;AAC3B"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/feature-flags",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Edge-first, driver-based feature flags for the Vela framework",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare-workers",
|
|
7
|
+
"driver",
|
|
8
|
+
"edge",
|
|
9
|
+
"feature-flags",
|
|
10
|
+
"feature-toggles",
|
|
11
|
+
"framework",
|
|
12
|
+
"vela"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/velajs/feature-flags#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/velajs/feature-flags/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "ksh",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/velajs/feature-flags.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
5
30
|
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
6
32
|
"main": "./dist/index.js",
|
|
7
33
|
"types": "./dist/index.d.ts",
|
|
8
34
|
"exports": {
|
|
@@ -15,52 +41,40 @@
|
|
|
15
41
|
"import": "./dist/testing/index.js"
|
|
16
42
|
}
|
|
17
43
|
},
|
|
18
|
-
"files": [
|
|
19
|
-
"dist",
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE",
|
|
22
|
-
"CHANGELOG.md"
|
|
23
|
-
],
|
|
24
|
-
"sideEffects": false,
|
|
25
|
-
"keywords": [
|
|
26
|
-
"vela",
|
|
27
|
-
"feature-flags",
|
|
28
|
-
"feature-toggles",
|
|
29
|
-
"driver",
|
|
30
|
-
"edge",
|
|
31
|
-
"cloudflare-workers",
|
|
32
|
-
"framework"
|
|
33
|
-
],
|
|
34
|
-
"author": "ksh",
|
|
35
|
-
"license": "MIT",
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/velajs/feature-flags.git"
|
|
39
|
-
},
|
|
40
|
-
"homepage": "https://github.com/velajs/feature-flags#readme",
|
|
41
|
-
"bugs": {
|
|
42
|
-
"url": "https://github.com/velajs/feature-flags/issues"
|
|
43
|
-
},
|
|
44
|
-
"engines": {
|
|
45
|
-
"node": ">=20"
|
|
46
|
-
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"@velajs/vela": ">=1.11.0",
|
|
49
|
-
"hono": ">=4"
|
|
50
|
-
},
|
|
51
44
|
"devDependencies": {
|
|
52
|
-
"@
|
|
45
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
46
|
+
"@changesets/cli": "^2.31.0",
|
|
53
47
|
"@swc/core": "^1.15.43",
|
|
54
48
|
"@velajs/testing": "^0.4.0",
|
|
55
49
|
"@velajs/vela": "^1.12.0",
|
|
56
50
|
"hono": "^4.12.27",
|
|
57
|
-
"
|
|
51
|
+
"oxfmt": "^0.58.0",
|
|
52
|
+
"oxlint": "^1.73.0",
|
|
53
|
+
"publint": "^0.3.21",
|
|
54
|
+
"tsdown": "^0.22.4",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
58
56
|
"unplugin-swc": "^1.5.9",
|
|
59
|
-
"vitest": "^4.1.
|
|
57
|
+
"vitest": "^4.1.10"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@velajs/vela": ">=1.11.0",
|
|
61
|
+
"hono": ">=4"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=24"
|
|
60
65
|
},
|
|
61
66
|
"scripts": {
|
|
62
|
-
"build": "
|
|
67
|
+
"build": "tsdown",
|
|
63
68
|
"test": "vitest run",
|
|
64
|
-
"typecheck": "tsc --noEmit"
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"lint": "oxlint .",
|
|
71
|
+
"format": "oxfmt .",
|
|
72
|
+
"format:check": "oxfmt --check .",
|
|
73
|
+
"publint": "publint",
|
|
74
|
+
"attw": "attw --pack . --profile esm-only",
|
|
75
|
+
"changeset": "changeset",
|
|
76
|
+
"version-packages": "changeset version",
|
|
77
|
+
"release": "pnpm build && changeset publish",
|
|
78
|
+
"verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
|
|
65
79
|
}
|
|
66
80
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { FlagKey } from '../feature-flags.types';
|
|
2
|
-
/** What the {@link FeatureFlagGuard} does when a gated flag is off. */
|
|
3
|
-
export type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';
|
|
4
|
-
export interface FeatureFlagOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Response when the flag is off. `'notFound'` (default) hides the route
|
|
7
|
-
* entirely (404); `'forbidden'` reveals it exists but denies access (403).
|
|
8
|
-
*/
|
|
9
|
-
onDisabled?: FeatureFlagDisabledBehavior;
|
|
10
|
-
}
|
|
11
|
-
/** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */
|
|
12
|
-
export interface FeatureFlagMetadata {
|
|
13
|
-
key: string;
|
|
14
|
-
onDisabled: FeatureFlagDisabledBehavior;
|
|
15
|
-
}
|
|
16
|
-
export declare const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
|
|
17
|
-
/**
|
|
18
|
-
* Gate a route (handler) or controller behind a boolean feature flag. Pair
|
|
19
|
-
* with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
|
|
20
|
-
* app-wide registration).
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* @UseGuards(FeatureFlagGuard)
|
|
25
|
-
* @Controller('/checkout')
|
|
26
|
-
* class CheckoutController {
|
|
27
|
-
* @FeatureFlag('new-checkout') // 404 when off
|
|
28
|
-
* @Get('/v2') v2() { ... }
|
|
29
|
-
*
|
|
30
|
-
* @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
31
|
-
* @Get('/beta') beta() { ... }
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export declare function FeatureFlag(key: FlagKey, options?: FeatureFlagOptions): (target: object, propertyKey?: string | symbol) => void;
|