@nestarc/feature-flag 0.1.0 → 0.3.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/README.md +252 -87
- package/dist/admin/admin-options.interface.d.ts +5 -0
- package/dist/admin/admin-options.interface.js +3 -0
- package/dist/admin/admin-options.interface.js.map +1 -0
- package/dist/admin/feature-flag-admin.controller.d.ts +14 -0
- package/dist/admin/feature-flag-admin.controller.js +106 -0
- package/dist/admin/feature-flag-admin.controller.js.map +1 -0
- package/dist/admin/feature-flag-admin.dto.d.ts +22 -0
- package/dist/admin/feature-flag-admin.dto.js +93 -0
- package/dist/admin/feature-flag-admin.dto.js.map +1 -0
- package/dist/admin/feature-flag-admin.module.d.ts +5 -0
- package/dist/admin/feature-flag-admin.module.js +33 -0
- package/dist/admin/feature-flag-admin.module.js.map +1 -0
- package/dist/admin/targeting-attributes.validator.d.ts +6 -0
- package/dist/admin/targeting-attributes.validator.js +36 -0
- package/dist/admin/targeting-attributes.validator.js.map +1 -0
- package/dist/cache/memory-cache.adapter.d.ts +11 -0
- package/dist/{services/flag-cache.service.js → cache/memory-cache.adapter.js} +18 -31
- package/dist/cache/memory-cache.adapter.js.map +1 -0
- package/dist/cache/redis-cache.adapter.d.ts +26 -0
- package/dist/cache/redis-cache.adapter.js +102 -0
- package/dist/cache/redis-cache.adapter.js.map +1 -0
- package/dist/events/feature-flag.events.d.ts +4 -5
- package/dist/feature-flag.constants.d.ts +3 -0
- package/dist/feature-flag.constants.js +4 -1
- package/dist/feature-flag.constants.js.map +1 -1
- package/dist/feature-flag.module.js +49 -10
- package/dist/feature-flag.module.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/cache-adapter.interface.d.ts +9 -0
- package/dist/interfaces/cache-adapter.interface.js +3 -0
- package/dist/interfaces/cache-adapter.interface.js.map +1 -0
- package/dist/interfaces/evaluation-context.interface.d.ts +6 -3
- package/dist/interfaces/feature-flag-options.interface.d.ts +3 -0
- package/dist/interfaces/feature-flag-repository.interface.d.ts +22 -0
- package/dist/interfaces/feature-flag-repository.interface.js +3 -0
- package/dist/interfaces/feature-flag-repository.interface.js.map +1 -0
- package/dist/interfaces/feature-flag.interface.d.ts +11 -6
- package/dist/interfaces/tenant-context-provider.interface.d.ts +3 -0
- package/dist/interfaces/tenant-context-provider.interface.js +3 -0
- package/dist/interfaces/tenant-context-provider.interface.js.map +1 -0
- package/dist/repositories/prisma-feature-flag.repository.d.ts +18 -0
- package/dist/repositories/prisma-feature-flag.repository.js +161 -0
- package/dist/repositories/prisma-feature-flag.repository.js.map +1 -0
- package/dist/services/default-tenant-context-provider.d.ts +7 -0
- package/dist/services/default-tenant-context-provider.js +35 -0
- package/dist/services/default-tenant-context-provider.js.map +1 -0
- package/dist/services/feature-flag.service.d.ts +21 -13
- package/dist/services/feature-flag.service.js +111 -126
- package/dist/services/feature-flag.service.js.map +1 -1
- package/dist/services/flag-context-resolver.d.ts +11 -0
- package/dist/services/flag-context-resolver.js +51 -0
- package/dist/services/flag-context-resolver.js.map +1 -0
- package/dist/services/flag-evaluator.service.d.ts +1 -0
- package/dist/services/flag-evaluator.service.js +29 -27
- package/dist/services/flag-evaluator.service.js.map +1 -1
- package/dist/services/flag-event-publisher.d.ts +7 -0
- package/dist/services/flag-event-publisher.js +37 -0
- package/dist/services/flag-event-publisher.js.map +1 -0
- package/dist/testing/test-feature-flag.module.js +26 -5
- package/dist/testing/test-feature-flag.module.js.map +1 -1
- package/dist/utils/targeting-attributes.d.ts +9 -0
- package/dist/utils/targeting-attributes.js +44 -0
- package/dist/utils/targeting-attributes.js.map +1 -0
- package/package.json +20 -3
- package/prisma/migrations/20260512000000_attribute_targeting/migration.sql +59 -0
- package/prisma/schema.prisma +7 -9
- package/dist/services/flag-cache.service.d.ts +0 -13
- package/dist/services/flag-cache.service.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FEATURE_FLAG_MODULE_OPTIONS = exports.FeatureFlagEvents = exports.BypassFeatureFlag = exports.FeatureFlag = exports.FeatureFlagGuard = exports.FlagContext = exports.FeatureFlagService = exports.FeatureFlagModule = void 0;
|
|
3
|
+
exports.FeatureFlagAdminModule = exports.PrismaFeatureFlagRepository = exports.RedisCacheAdapter = exports.MemoryCacheAdapter = exports.TENANT_CONTEXT_PROVIDER = exports.FEATURE_FLAG_REPOSITORY = exports.CACHE_ADAPTER = exports.FEATURE_FLAG_MODULE_OPTIONS = exports.FeatureFlagEvents = exports.BypassFeatureFlag = exports.FeatureFlag = exports.FeatureFlagGuard = exports.FlagContext = exports.FeatureFlagService = exports.FeatureFlagModule = void 0;
|
|
4
4
|
// Module
|
|
5
5
|
var feature_flag_module_1 = require("./feature-flag.module");
|
|
6
6
|
Object.defineProperty(exports, "FeatureFlagModule", { enumerable: true, get: function () { return feature_flag_module_1.FeatureFlagModule; } });
|
|
@@ -23,4 +23,16 @@ Object.defineProperty(exports, "FeatureFlagEvents", { enumerable: true, get: fun
|
|
|
23
23
|
// Constants
|
|
24
24
|
var feature_flag_constants_1 = require("./feature-flag.constants");
|
|
25
25
|
Object.defineProperty(exports, "FEATURE_FLAG_MODULE_OPTIONS", { enumerable: true, get: function () { return feature_flag_constants_1.FEATURE_FLAG_MODULE_OPTIONS; } });
|
|
26
|
+
Object.defineProperty(exports, "CACHE_ADAPTER", { enumerable: true, get: function () { return feature_flag_constants_1.CACHE_ADAPTER; } });
|
|
27
|
+
Object.defineProperty(exports, "FEATURE_FLAG_REPOSITORY", { enumerable: true, get: function () { return feature_flag_constants_1.FEATURE_FLAG_REPOSITORY; } });
|
|
28
|
+
Object.defineProperty(exports, "TENANT_CONTEXT_PROVIDER", { enumerable: true, get: function () { return feature_flag_constants_1.TENANT_CONTEXT_PROVIDER; } });
|
|
29
|
+
var memory_cache_adapter_1 = require("./cache/memory-cache.adapter");
|
|
30
|
+
Object.defineProperty(exports, "MemoryCacheAdapter", { enumerable: true, get: function () { return memory_cache_adapter_1.MemoryCacheAdapter; } });
|
|
31
|
+
var redis_cache_adapter_1 = require("./cache/redis-cache.adapter");
|
|
32
|
+
Object.defineProperty(exports, "RedisCacheAdapter", { enumerable: true, get: function () { return redis_cache_adapter_1.RedisCacheAdapter; } });
|
|
33
|
+
var prisma_feature_flag_repository_1 = require("./repositories/prisma-feature-flag.repository");
|
|
34
|
+
Object.defineProperty(exports, "PrismaFeatureFlagRepository", { enumerable: true, get: function () { return prisma_feature_flag_repository_1.PrismaFeatureFlagRepository; } });
|
|
35
|
+
// Admin module
|
|
36
|
+
var feature_flag_admin_module_1 = require("./admin/feature-flag-admin.module");
|
|
37
|
+
Object.defineProperty(exports, "FeatureFlagAdminModule", { enumerable: true, get: function () { return feature_flag_admin_module_1.FeatureFlagAdminModule; } });
|
|
26
38
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,6DAA2H;AAAlH,wHAAA,iBAAiB,OAAA;AAE1B,WAAW;AACX,wEAAqE;AAA5D,0HAAA,kBAAkB,OAAA;AAC3B,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,QAAQ;AACR,kEAA+D;AAAtD,sHAAA,gBAAgB,OAAA;AAEzB,aAAa;AACb,8EAAkE;AAAzD,qHAAA,WAAW,OAAA;AACpB,4FAA+E;AAAtE,kIAAA,iBAAiB,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,6DAA2H;AAAlH,wHAAA,iBAAiB,OAAA;AAE1B,WAAW;AACX,wEAAqE;AAA5D,0HAAA,kBAAkB,OAAA;AAC3B,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,QAAQ;AACR,kEAA+D;AAAtD,sHAAA,gBAAgB,OAAA;AAEzB,aAAa;AACb,8EAAkE;AAAzD,qHAAA,WAAW,OAAA;AACpB,4FAA+E;AAAtE,kIAAA,iBAAiB,OAAA;AAqB1B,SAAS;AACT,oEAKsC;AAJpC,wHAAA,iBAAiB,OAAA;AAMnB,YAAY;AACZ,mEAKkC;AAJhC,qIAAA,2BAA2B,OAAA;AAC3B,uHAAA,aAAa,OAAA;AACb,iIAAA,uBAAuB,OAAA;AACvB,iIAAA,uBAAuB,OAAA;AAKzB,qEAAkE;AAAzD,0HAAA,kBAAkB,OAAA;AAC3B,mEAA+F;AAAtF,wHAAA,iBAAiB,OAAA;AAQ1B,gGAA4F;AAAnF,6IAAA,2BAA2B,OAAA;AAKpC,eAAe;AACf,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FeatureFlagWithOverrides } from './feature-flag.interface';
|
|
2
|
+
export interface CacheAdapter {
|
|
3
|
+
get(key: string): Promise<FeatureFlagWithOverrides | null>;
|
|
4
|
+
set(key: string, data: FeatureFlagWithOverrides, ttlMs: number): Promise<void>;
|
|
5
|
+
getAll(): Promise<FeatureFlagWithOverrides[] | null>;
|
|
6
|
+
setAll(data: FeatureFlagWithOverrides[], ttlMs: number): Promise<void>;
|
|
7
|
+
invalidate(key?: string): Promise<void>;
|
|
8
|
+
onModuleDestroy?(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-adapter.interface.js","sourceRoot":"","sources":["../../src/interfaces/cache-adapter.interface.ts"],"names":[],"mappings":""}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { TargetingAttributes } from './feature-flag.interface';
|
|
1
2
|
export interface EvaluationContext {
|
|
2
|
-
/** User ID
|
|
3
|
+
/** User ID - used for user-scoped targeting and percentage hash */
|
|
3
4
|
userId?: string | null;
|
|
4
|
-
/** Tenant ID
|
|
5
|
+
/** Tenant ID - used for tenant-scoped targeting. Ignored if tenancy is not installed */
|
|
5
6
|
tenantId?: string | null;
|
|
6
|
-
/** Environment
|
|
7
|
+
/** Environment - auto-injected from module options. Can be explicitly overridden */
|
|
7
8
|
environment?: string;
|
|
9
|
+
/** Additional exact-match targeting attributes */
|
|
10
|
+
attributes?: TargetingAttributes;
|
|
8
11
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
2
|
import { Request } from 'express';
|
|
3
|
+
import { CacheAdapter } from './cache-adapter.interface';
|
|
3
4
|
export interface FeatureFlagModuleOptions {
|
|
4
5
|
/** Current environment (e.g., 'development', 'staging', 'production') */
|
|
5
6
|
environment: string;
|
|
@@ -11,6 +12,8 @@ export interface FeatureFlagModuleOptions {
|
|
|
11
12
|
defaultOnMissing?: boolean;
|
|
12
13
|
/** Emit evaluation events via @nestjs/event-emitter. Default: false */
|
|
13
14
|
emitEvents?: boolean;
|
|
15
|
+
/** Custom cache adapter implementation. If not provided, an in-memory cache is used. */
|
|
16
|
+
cacheAdapter?: CacheAdapter;
|
|
14
17
|
}
|
|
15
18
|
export interface FeatureFlagModuleOptionsFactory {
|
|
16
19
|
createFeatureFlagOptions(): Promise<FeatureFlagModuleOptions & {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CreateFeatureFlagInput, UpdateFeatureFlagInput, FeatureFlagWithOverrides, TargetingAttributes } from './feature-flag.interface';
|
|
2
|
+
export interface OverrideCriteria {
|
|
3
|
+
attributes: TargetingAttributes;
|
|
4
|
+
}
|
|
5
|
+
export interface UpdateOverrideInput {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
priority: number;
|
|
8
|
+
}
|
|
9
|
+
export interface FeatureFlagRepository {
|
|
10
|
+
createFlag(input: CreateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
|
|
11
|
+
updateFlag(key: string, input: UpdateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
|
|
12
|
+
archiveFlag(key: string): Promise<FeatureFlagWithOverrides>;
|
|
13
|
+
findFlagByKey(key: string): Promise<FeatureFlagWithOverrides | null>;
|
|
14
|
+
findFlagIdByKey(key: string): Promise<string | null>;
|
|
15
|
+
findAllActiveFlags(): Promise<FeatureFlagWithOverrides[]>;
|
|
16
|
+
findOverride(flagId: string, criteria: OverrideCriteria): Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean, priority: number): Promise<void>;
|
|
20
|
+
updateOverride(id: string, input: UpdateOverrideInput): Promise<void>;
|
|
21
|
+
deleteOverride(id: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flag-repository.interface.js","sourceRoot":"","sources":["../../src/interfaces/feature-flag-repository.interface.ts"],"names":[],"mappings":""}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type TargetingAttributeValue = string | number | boolean | null;
|
|
2
|
+
export type TargetingAttributes = Record<string, TargetingAttributeValue>;
|
|
1
3
|
export interface CreateFeatureFlagInput {
|
|
2
4
|
key: string;
|
|
3
5
|
description?: string;
|
|
@@ -12,10 +14,9 @@ export interface UpdateFeatureFlagInput {
|
|
|
12
14
|
metadata?: Record<string, unknown>;
|
|
13
15
|
}
|
|
14
16
|
export interface SetOverrideInput {
|
|
15
|
-
|
|
16
|
-
userId?: string;
|
|
17
|
-
environment?: string;
|
|
17
|
+
attributes: TargetingAttributes;
|
|
18
18
|
enabled: boolean;
|
|
19
|
+
priority?: number;
|
|
19
20
|
}
|
|
20
21
|
export interface FeatureFlagGuardOptions {
|
|
21
22
|
/** HTTP status code when flag is OFF. Default: 403 */
|
|
@@ -38,8 +39,12 @@ export interface FeatureFlagWithOverrides {
|
|
|
38
39
|
export interface FlagOverride {
|
|
39
40
|
id: string;
|
|
40
41
|
flagId: string;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
environment: string | null;
|
|
42
|
+
attributes: TargetingAttributes;
|
|
43
|
+
priority: number;
|
|
44
44
|
enabled: boolean;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
}
|
|
48
|
+
export interface RemoveOverrideInput {
|
|
49
|
+
attributes: TargetingAttributes;
|
|
45
50
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-context-provider.interface.js","sourceRoot":"","sources":["../../src/interfaces/tenant-context-provider.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FeatureFlagRepository, OverrideCriteria, UpdateOverrideInput } from '../interfaces/feature-flag-repository.interface';
|
|
2
|
+
import { CreateFeatureFlagInput, UpdateFeatureFlagInput, FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
|
|
3
|
+
export declare class PrismaFeatureFlagRepository implements FeatureFlagRepository {
|
|
4
|
+
private readonly prisma;
|
|
5
|
+
constructor(prisma: any);
|
|
6
|
+
createFlag(input: CreateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
|
|
7
|
+
updateFlag(key: string, input: UpdateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
|
|
8
|
+
archiveFlag(key: string): Promise<FeatureFlagWithOverrides>;
|
|
9
|
+
findFlagByKey(key: string): Promise<FeatureFlagWithOverrides | null>;
|
|
10
|
+
findFlagIdByKey(key: string): Promise<string | null>;
|
|
11
|
+
findAllActiveFlags(): Promise<FeatureFlagWithOverrides[]>;
|
|
12
|
+
findOverride(flagId: string, criteria: OverrideCriteria): Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
} | null>;
|
|
15
|
+
createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean, priority: number): Promise<void>;
|
|
16
|
+
updateOverride(id: string, input: UpdateOverrideInput): Promise<void>;
|
|
17
|
+
deleteOverride(id: string): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PrismaFeatureFlagRepository = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
function isPrismaError(error, code) {
|
|
15
|
+
return (typeof error === 'object' &&
|
|
16
|
+
error !== null &&
|
|
17
|
+
'code' in error &&
|
|
18
|
+
error.code === code);
|
|
19
|
+
}
|
|
20
|
+
let PrismaFeatureFlagRepository = class PrismaFeatureFlagRepository {
|
|
21
|
+
constructor(prisma) {
|
|
22
|
+
this.prisma = prisma;
|
|
23
|
+
}
|
|
24
|
+
async createFlag(input) {
|
|
25
|
+
const percentage = input.percentage ?? 0;
|
|
26
|
+
if (percentage < 0 || percentage > 100) {
|
|
27
|
+
throw new common_1.BadRequestException(`percentage must be between 0 and 100, got ${percentage}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return await this.prisma.featureFlag.create({
|
|
31
|
+
data: {
|
|
32
|
+
key: input.key,
|
|
33
|
+
description: input.description,
|
|
34
|
+
enabled: input.enabled ?? false,
|
|
35
|
+
percentage,
|
|
36
|
+
metadata: input.metadata ?? {},
|
|
37
|
+
},
|
|
38
|
+
include: { overrides: true },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (isPrismaError(error, 'P2002')) {
|
|
43
|
+
throw new common_1.ConflictException(`Feature flag "${input.key}" already exists`);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async updateFlag(key, input) {
|
|
49
|
+
if (input.percentage !== undefined && (input.percentage < 0 || input.percentage > 100)) {
|
|
50
|
+
throw new common_1.BadRequestException(`percentage must be between 0 and 100, got ${input.percentage}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return await this.prisma.featureFlag.update({
|
|
54
|
+
where: { key },
|
|
55
|
+
data: {
|
|
56
|
+
...(input.description !== undefined && { description: input.description }),
|
|
57
|
+
...(input.enabled !== undefined && { enabled: input.enabled }),
|
|
58
|
+
...(input.percentage !== undefined && { percentage: input.percentage }),
|
|
59
|
+
...(input.metadata !== undefined && { metadata: input.metadata }),
|
|
60
|
+
},
|
|
61
|
+
include: { overrides: true },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (isPrismaError(error, 'P2025')) {
|
|
66
|
+
throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async archiveFlag(key) {
|
|
72
|
+
try {
|
|
73
|
+
return await this.prisma.featureFlag.update({
|
|
74
|
+
where: { key },
|
|
75
|
+
data: { archivedAt: new Date() },
|
|
76
|
+
include: { overrides: true },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isPrismaError(error, 'P2025')) {
|
|
81
|
+
throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async findFlagByKey(key) {
|
|
87
|
+
return this.prisma.featureFlag.findUnique({
|
|
88
|
+
where: { key },
|
|
89
|
+
include: { overrides: true },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async findFlagIdByKey(key) {
|
|
93
|
+
const flag = await this.prisma.featureFlag.findUnique({ where: { key } });
|
|
94
|
+
return flag?.id ?? null;
|
|
95
|
+
}
|
|
96
|
+
async findAllActiveFlags() {
|
|
97
|
+
return this.prisma.featureFlag.findMany({
|
|
98
|
+
where: { archivedAt: null },
|
|
99
|
+
include: { overrides: true },
|
|
100
|
+
orderBy: { createdAt: 'desc' },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async findOverride(flagId, criteria) {
|
|
104
|
+
return this.prisma.featureFlagOverride.findFirst({
|
|
105
|
+
where: {
|
|
106
|
+
flagId,
|
|
107
|
+
attributes: { equals: criteria.attributes },
|
|
108
|
+
},
|
|
109
|
+
select: { id: true },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async createOverride(flagId, criteria, enabled, priority) {
|
|
113
|
+
try {
|
|
114
|
+
await this.prisma.featureFlagOverride.create({
|
|
115
|
+
data: {
|
|
116
|
+
flagId,
|
|
117
|
+
attributes: criteria.attributes,
|
|
118
|
+
priority,
|
|
119
|
+
enabled,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (isPrismaError(error, 'P2002')) {
|
|
125
|
+
const existing = await this.findOverride(flagId, criteria);
|
|
126
|
+
if (existing) {
|
|
127
|
+
await this.updateOverride(existing.id, { enabled, priority });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async updateOverride(id, input) {
|
|
135
|
+
await this.prisma.featureFlagOverride.update({
|
|
136
|
+
where: { id },
|
|
137
|
+
data: {
|
|
138
|
+
enabled: input.enabled,
|
|
139
|
+
priority: input.priority,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async deleteOverride(id) {
|
|
144
|
+
try {
|
|
145
|
+
await this.prisma.featureFlagOverride.delete({ where: { id } });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (isPrismaError(error, 'P2025')) {
|
|
149
|
+
// Already deleted by a concurrent request — idempotent
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
exports.PrismaFeatureFlagRepository = PrismaFeatureFlagRepository;
|
|
157
|
+
exports.PrismaFeatureFlagRepository = PrismaFeatureFlagRepository = __decorate([
|
|
158
|
+
(0, common_1.Injectable)(),
|
|
159
|
+
__metadata("design:paramtypes", [Object])
|
|
160
|
+
], PrismaFeatureFlagRepository);
|
|
161
|
+
//# sourceMappingURL=prisma-feature-flag.repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma-feature-flag.repository.js","sourceRoot":"","sources":["../../src/repositories/prisma-feature-flag.repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ModuleRef } from '@nestjs/core';
|
|
2
|
+
import { TenantContextProvider } from '../interfaces/tenant-context-provider.interface';
|
|
3
|
+
export declare class DefaultTenantContextProvider implements TenantContextProvider {
|
|
4
|
+
private readonly moduleRef;
|
|
5
|
+
constructor(moduleRef: ModuleRef);
|
|
6
|
+
getCurrentTenantId(): string | null;
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DefaultTenantContextProvider = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const core_1 = require("@nestjs/core");
|
|
15
|
+
let DefaultTenantContextProvider = class DefaultTenantContextProvider {
|
|
16
|
+
constructor(moduleRef) {
|
|
17
|
+
this.moduleRef = moduleRef;
|
|
18
|
+
}
|
|
19
|
+
getCurrentTenantId() {
|
|
20
|
+
try {
|
|
21
|
+
const { TenancyService } = require('@nestarc/tenancy');
|
|
22
|
+
const tenancyService = this.moduleRef.get(TenancyService, { strict: false });
|
|
23
|
+
return tenancyService?.getCurrentTenant() ?? null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
exports.DefaultTenantContextProvider = DefaultTenantContextProvider;
|
|
31
|
+
exports.DefaultTenantContextProvider = DefaultTenantContextProvider = __decorate([
|
|
32
|
+
(0, common_1.Injectable)(),
|
|
33
|
+
__metadata("design:paramtypes", [core_1.ModuleRef])
|
|
34
|
+
], DefaultTenantContextProvider);
|
|
35
|
+
//# sourceMappingURL=default-tenant-context-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-tenant-context-provider.js","sourceRoot":"","sources":["../../src/services/default-tenant-context-provider.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,uCAAyC;AAIlC,IAAM,4BAA4B,GAAlC,MAAM,4BAA4B;IACvC,YAA6B,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAG,CAAC;IAErD,kBAAkB;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;YACvD,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7E,OAAO,cAAc,EAAE,gBAAgB,EAAE,IAAI,IAAI,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF,CAAA;AAZY,oEAA4B;uCAA5B,4BAA4B;IADxC,IAAA,mBAAU,GAAE;qCAE6B,gBAAS;GADtC,4BAA4B,CAYxC"}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { ModuleRef } from '@nestjs/core';
|
|
2
1
|
import { FeatureFlagModuleOptions } from '../interfaces/feature-flag-options.interface';
|
|
3
|
-
import { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
|
|
2
|
+
import { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, RemoveOverrideInput, FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
|
|
4
3
|
import { EvaluationContext } from '../interfaces/evaluation-context.interface';
|
|
5
|
-
import {
|
|
4
|
+
import { CacheAdapter } from '../interfaces/cache-adapter.interface';
|
|
5
|
+
import { FeatureFlagRepository } from '../interfaces/feature-flag-repository.interface';
|
|
6
6
|
import { FlagEvaluatorService } from './flag-evaluator.service';
|
|
7
|
-
import {
|
|
7
|
+
import { FlagContextResolver } from './flag-context-resolver';
|
|
8
|
+
import { FlagEventPublisher } from './flag-event-publisher';
|
|
8
9
|
export declare class FeatureFlagService {
|
|
9
10
|
private readonly options;
|
|
10
|
-
private readonly
|
|
11
|
-
private readonly
|
|
11
|
+
private readonly repository;
|
|
12
|
+
private readonly cacheAdapter;
|
|
12
13
|
private readonly evaluator;
|
|
13
|
-
private readonly
|
|
14
|
-
private readonly
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
private readonly contextResolver;
|
|
15
|
+
private readonly eventPublisher;
|
|
16
|
+
constructor(options: FeatureFlagModuleOptions, repository: FeatureFlagRepository, cacheAdapter: CacheAdapter, evaluator: FlagEvaluatorService, contextResolver: FlagContextResolver, eventPublisher: FlagEventPublisher);
|
|
17
|
+
private get cacheTtlMs();
|
|
17
18
|
isEnabled(flagKey: string, explicitContext?: EvaluationContext): Promise<boolean>;
|
|
18
19
|
evaluateAll(explicitContext?: EvaluationContext): Promise<Record<string, boolean>>;
|
|
19
20
|
create(input: CreateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
|
|
@@ -21,9 +22,16 @@ export declare class FeatureFlagService {
|
|
|
21
22
|
archive(key: string): Promise<FeatureFlagWithOverrides>;
|
|
22
23
|
setOverride(key: string, input: SetOverrideInput): Promise<void>;
|
|
23
24
|
findAll(): Promise<FeatureFlagWithOverrides[]>;
|
|
24
|
-
invalidateCache(): void
|
|
25
|
+
invalidateCache(): Promise<void>;
|
|
26
|
+
findByKey(key: string): Promise<FeatureFlagWithOverrides>;
|
|
27
|
+
removeOverride(key: string, input: RemoveOverrideInput): Promise<void>;
|
|
28
|
+
private normalizeOverrideAttributes;
|
|
29
|
+
/**
|
|
30
|
+
* Best-effort cache invalidation for mutation paths.
|
|
31
|
+
* DB write already succeeded — cache failure should not fail the caller.
|
|
32
|
+
* Stale entries self-heal via TTL (default 30s).
|
|
33
|
+
*/
|
|
34
|
+
private safeInvalidateCache;
|
|
25
35
|
private resolveFlag;
|
|
26
36
|
private resolveAllFlags;
|
|
27
|
-
private buildContext;
|
|
28
|
-
private getTenantId;
|
|
29
37
|
}
|