@nestarc/feature-flag 0.2.0 → 0.4.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 +206 -81
- package/dist/admin/feature-flag-admin.controller.d.ts +8 -6
- package/dist/admin/feature-flag-admin.controller.js +22 -4
- package/dist/admin/feature-flag-admin.controller.js.map +1 -1
- package/dist/admin/feature-flag-admin.dto.d.ts +30 -0
- package/dist/admin/feature-flag-admin.dto.js +116 -0
- package/dist/admin/feature-flag-admin.dto.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/events/feature-flag.events.d.ts +38 -6
- package/dist/events/feature-flag.events.js +1 -0
- package/dist/events/feature-flag.events.js.map +1 -1
- package/dist/flag-registry.d.ts +24 -0
- package/dist/flag-registry.js +62 -0
- package/dist/flag-registry.js.map +1 -0
- package/dist/guards/feature-flag.guard.js +5 -1
- package/dist/guards/feature-flag.guard.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/evaluation-context.interface.d.ts +8 -3
- package/dist/interfaces/evaluation-details.interface.d.ts +29 -0
- package/dist/interfaces/evaluation-details.interface.js +3 -0
- package/dist/interfaces/evaluation-details.interface.js.map +1 -0
- package/dist/interfaces/feature-flag-options.interface.d.ts +3 -0
- package/dist/interfaces/feature-flag-repository.interface.d.ts +8 -6
- package/dist/interfaces/feature-flag.interface.d.ts +18 -9
- package/dist/interfaces/flag-registry.interface.d.ts +17 -0
- package/dist/interfaces/flag-registry.interface.js +3 -0
- package/dist/interfaces/flag-registry.interface.js.map +1 -0
- package/dist/openfeature.d.ts +26 -0
- package/dist/openfeature.js +96 -0
- package/dist/openfeature.js.map +1 -0
- package/dist/repositories/prisma-feature-flag.repository.d.ts +3 -3
- package/dist/repositories/prisma-feature-flag.repository.js +18 -14
- package/dist/repositories/prisma-feature-flag.repository.js.map +1 -1
- package/dist/services/feature-flag.service.d.ts +15 -7
- package/dist/services/feature-flag.service.js +126 -38
- package/dist/services/feature-flag.service.js.map +1 -1
- package/dist/services/flag-context-resolver.js +14 -3
- package/dist/services/flag-context-resolver.js.map +1 -1
- package/dist/services/flag-evaluator.service.d.ts +8 -8
- package/dist/services/flag-evaluator.service.js +90 -36
- package/dist/services/flag-evaluator.service.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/test-feature-flag.module.d.ts +21 -0
- package/dist/testing/test-feature-flag.module.js +78 -19
- 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 +18 -2
- package/prisma/migrations/20260512000000_attribute_targeting/migration.sql +59 -0
- package/prisma/schema.prisma +7 -9
|
@@ -9,55 +9,109 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.FlagEvaluatorService = void 0;
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
11
|
const hash_1 = require("../utils/hash");
|
|
12
|
+
const targeting_attributes_1 = require("../utils/targeting-attributes");
|
|
12
13
|
let FlagEvaluatorService = class FlagEvaluatorService {
|
|
13
|
-
evaluate(flag, context) {
|
|
14
|
-
// 1. Archived → always false
|
|
14
|
+
evaluate(flag, context, options = {}) {
|
|
15
15
|
if (flag.archivedAt) {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
// 2. User override (most specific)
|
|
19
|
-
if (context.userId) {
|
|
20
|
-
const userOverride = flag.overrides.find((o) => o.userId === context.userId &&
|
|
21
|
-
(o.tenantId === null || o.tenantId === context.tenantId) &&
|
|
22
|
-
(o.environment === null || o.environment === context.environment));
|
|
23
|
-
if (userOverride) {
|
|
24
|
-
return { result: userOverride.enabled, source: 'user_override' };
|
|
25
|
-
}
|
|
16
|
+
return this.result(flag.key, false, 'global', 'ARCHIVED');
|
|
26
17
|
}
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (tenantOverride) {
|
|
33
|
-
return { result: tenantOverride.enabled, source: 'tenant_override' };
|
|
34
|
-
}
|
|
18
|
+
const override = this.findMatchingOverride(flag.overrides, context);
|
|
19
|
+
if (override) {
|
|
20
|
+
return this.result(flag.key, override.enabled, 'override', 'OVERRIDE_MATCH', {
|
|
21
|
+
matchedOverrideId: override.id,
|
|
22
|
+
});
|
|
35
23
|
}
|
|
36
|
-
// 4. Environment override
|
|
37
|
-
if (context.environment) {
|
|
38
|
-
const envOverride = flag.overrides.find((o) => o.environment === context.environment && o.tenantId === null && o.userId === null);
|
|
39
|
-
if (envOverride) {
|
|
40
|
-
return { result: envOverride.enabled, source: 'env_override' };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// 5. Percentage rollout
|
|
44
24
|
if (flag.percentage > 0) {
|
|
45
25
|
if (flag.percentage === 100) {
|
|
46
|
-
return
|
|
26
|
+
return this.result(flag.key, true, 'percentage', 'PERCENTAGE_MATCH');
|
|
27
|
+
}
|
|
28
|
+
const targetingKey = this.resolveTargetingKey(flag, context, options.bucketBy);
|
|
29
|
+
if (!targetingKey) {
|
|
30
|
+
return this.result(flag.key, flag.enabled, 'global', 'PERCENTAGE_NO_TARGETING_KEY');
|
|
47
31
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
32
|
+
const bucket = (0, hash_1.murmurhash3)(flag.key + targetingKey) % 100;
|
|
33
|
+
const enabled = bucket < flag.percentage;
|
|
34
|
+
return this.result(flag.key, enabled, 'percentage', enabled ? 'PERCENTAGE_MATCH' : 'PERCENTAGE_MISS', { bucket, targetingKey });
|
|
35
|
+
}
|
|
36
|
+
return this.result(flag.key, flag.enabled, 'global', 'GLOBAL');
|
|
37
|
+
}
|
|
38
|
+
findMatchingOverride(overrides, context) {
|
|
39
|
+
const contextAttributes = context.attributes ?? {};
|
|
40
|
+
return (overrides
|
|
41
|
+
.filter((override) => (0, targeting_attributes_1.matchesTargetingAttributes)(override.attributes, contextAttributes))
|
|
42
|
+
.sort(compareOverrides)[0] ?? null);
|
|
43
|
+
}
|
|
44
|
+
resolveTargetingKey(flag, context, bucketBy) {
|
|
45
|
+
if (context.targetingKey) {
|
|
46
|
+
return context.targetingKey;
|
|
47
|
+
}
|
|
48
|
+
const configuredBucketBy = bucketBy ?? this.getMetadataBucketBy(flag.metadata);
|
|
49
|
+
if (configuredBucketBy) {
|
|
50
|
+
const configuredValue = this.readBucketValue(context, configuredBucketBy);
|
|
51
|
+
if (configuredValue) {
|
|
52
|
+
return configuredValue;
|
|
51
53
|
}
|
|
52
|
-
const bucket = (0, hash_1.murmurhash3)(flag.key + hashKey) % 100;
|
|
53
|
-
return { result: bucket < flag.percentage, source: 'percentage' };
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
return context.userId ?? context.tenantId ?? '';
|
|
56
|
+
}
|
|
57
|
+
getMetadataBucketBy(metadata) {
|
|
58
|
+
const bucketBy = metadata.bucketBy;
|
|
59
|
+
return typeof bucketBy === 'string' && bucketBy.length > 0 ? bucketBy : undefined;
|
|
60
|
+
}
|
|
61
|
+
readBucketValue(context, bucketBy) {
|
|
62
|
+
if (bucketBy === 'userId') {
|
|
63
|
+
return context.userId ?? '';
|
|
64
|
+
}
|
|
65
|
+
if (bucketBy === 'tenantId') {
|
|
66
|
+
return context.tenantId ?? '';
|
|
67
|
+
}
|
|
68
|
+
if (bucketBy === 'environment') {
|
|
69
|
+
return context.environment ?? '';
|
|
70
|
+
}
|
|
71
|
+
if (bucketBy === 'targetingKey') {
|
|
72
|
+
return context.targetingKey ?? '';
|
|
73
|
+
}
|
|
74
|
+
return stringifyBucketValue(context.attributes?.[bucketBy]);
|
|
75
|
+
}
|
|
76
|
+
result(flagKey, value, source, reason, details = {}) {
|
|
77
|
+
return {
|
|
78
|
+
flagKey,
|
|
79
|
+
value,
|
|
80
|
+
result: value,
|
|
81
|
+
source,
|
|
82
|
+
reason,
|
|
83
|
+
defaultUsed: false,
|
|
84
|
+
...details,
|
|
85
|
+
};
|
|
57
86
|
}
|
|
58
87
|
};
|
|
59
88
|
exports.FlagEvaluatorService = FlagEvaluatorService;
|
|
60
89
|
exports.FlagEvaluatorService = FlagEvaluatorService = __decorate([
|
|
61
90
|
(0, common_1.Injectable)()
|
|
62
91
|
], FlagEvaluatorService);
|
|
92
|
+
function stringifyBucketValue(value) {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
function compareOverrides(a, b) {
|
|
99
|
+
const specificityDelta = Object.keys(b.attributes).length - Object.keys(a.attributes).length;
|
|
100
|
+
if (specificityDelta !== 0) {
|
|
101
|
+
return specificityDelta;
|
|
102
|
+
}
|
|
103
|
+
const priorityDelta = b.priority - a.priority;
|
|
104
|
+
if (priorityDelta !== 0) {
|
|
105
|
+
return priorityDelta;
|
|
106
|
+
}
|
|
107
|
+
const createdAtDelta = getOverrideCreatedAtTime(a) - getOverrideCreatedAtTime(b);
|
|
108
|
+
if (createdAtDelta !== 0) {
|
|
109
|
+
return createdAtDelta;
|
|
110
|
+
}
|
|
111
|
+
return a.id.localeCompare(b.id);
|
|
112
|
+
}
|
|
113
|
+
function getOverrideCreatedAtTime(override) {
|
|
114
|
+
const createdAt = override.createdAt;
|
|
115
|
+
return createdAt instanceof Date ? createdAt.getTime() : new Date(createdAt).getTime();
|
|
116
|
+
}
|
|
63
117
|
//# sourceMappingURL=flag-evaluator.service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;
|
|
1
|
+
{"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAO5C,wCAA4C;AAC5C,wEAA2E;AAWpE,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B,QAAQ,CACN,IAA8B,EAC9B,OAA0B,EAC1B,UAAgC,EAAE;QAElC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE;gBAC3E,iBAAiB,EAAE,QAAQ,CAAC,EAAE;aAC/B,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;gBAC5B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,kBAAkB,CAAC,CAAC;YACvE,CAAC;YAED,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC/E,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,OAAO,IAAI,CAAC,MAAM,CAChB,IAAI,CAAC,GAAG,EACR,IAAI,CAAC,OAAO,EACZ,QAAQ,EACR,6BAA6B,CAC9B,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,IAAA,kBAAW,EAAC,IAAI,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,GAAG,CAAC;YAC1D,MAAM,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;YACzC,OAAO,IAAI,CAAC,MAAM,CAChB,IAAI,CAAC,GAAG,EACR,OAAO,EACP,YAAY,EACZ,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,iBAAiB,EAChD,EAAE,MAAM,EAAE,YAAY,EAAE,CACzB,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAEO,oBAAoB,CAC1B,SAAyB,EACzB,OAA0B;QAE1B,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;QAEnD,OAAO,CACL,SAAS;aACN,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAA,iDAA0B,EAAC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;aACxF,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CACrC,CAAC;IACJ,CAAC;IAEO,mBAAmB,CACzB,IAA8B,EAC9B,OAA0B,EAC1B,QAAmB;QAEnB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,YAAY,CAAC;QAC9B,CAAC;QAED,MAAM,kBAAkB,GAAG,QAAQ,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/E,IAAI,kBAAkB,EAAE,CAAC;YACvB,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;YAC1E,IAAI,eAAe,EAAE,CAAC;gBACpB,OAAO,eAAe,CAAC;YACzB,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;IAClD,CAAC;IAEO,mBAAmB,CAAC,QAAiC;QAC3D,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QACnC,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IACpF,CAAC;IAEO,eAAe,CAAC,OAA0B,EAAE,QAAkB;QACpE,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC1B,OAAO,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;QAC9B,CAAC;QACD,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;YAC5B,OAAO,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;QAChC,CAAC;QACD,IAAI,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC/B,OAAO,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;QACnC,CAAC;QACD,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;YAChC,OAAO,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QACpC,CAAC;QAED,OAAO,oBAAoB,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;IAEO,MAAM,CACZ,OAAe,EACf,KAAc,EACd,MAAwB,EACxB,MAAkC,EAClC,UAAqC,EAAE;QAEvC,OAAO;YACL,OAAO;YACP,KAAK;YACL,MAAM,EAAE,KAAK;YACb,MAAM;YACN,MAAM;YACN,WAAW,EAAE,KAAK;YAClB,GAAG,OAAO;SACX,CAAC;IACJ,CAAC;CACF,CAAA;AAtHY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;GACA,oBAAoB,CAsHhC;AAED,SAAS,oBAAoB,CAAC,KAA0C;IACtE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,gBAAgB,CAAC,CAAe,EAAE,CAAe;IACxD,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IAC7F,IAAI,gBAAgB,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC9C,IAAI,aAAa,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,cAAc,GAAG,wBAAwB,CAAC,CAAC,CAAC,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACjF,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,wBAAwB,CAAC,QAAsB;IACtD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA0B,CAAC;IACtD,OAAO,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;AACzF,CAAC"}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { TestFeatureFlagModule } from './test-feature-flag.module';
|
|
1
|
+
export { TestFeatureFlagController, TestFeatureFlagModule, TestFeatureFlagRegistryOptions, } from './test-feature-flag.module';
|
package/dist/testing/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TestFeatureFlagModule = void 0;
|
|
3
|
+
exports.TestFeatureFlagModule = exports.TestFeatureFlagController = void 0;
|
|
4
4
|
var test_feature_flag_module_1 = require("./test-feature-flag.module");
|
|
5
|
+
Object.defineProperty(exports, "TestFeatureFlagController", { enumerable: true, get: function () { return test_feature_flag_module_1.TestFeatureFlagController; } });
|
|
5
6
|
Object.defineProperty(exports, "TestFeatureFlagModule", { enumerable: true, get: function () { return test_feature_flag_module_1.TestFeatureFlagModule; } });
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";;;AAAA,uEAIoC;AAHlC,qIAAA,yBAAyB,OAAA;AACzB,iIAAA,qBAAqB,OAAA"}
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
|
|
3
|
+
import { BooleanEvaluationDetails, EvaluateBooleanOptions } from '../interfaces/evaluation-details.interface';
|
|
4
|
+
import { FlagKey, FlagRegistry } from '../interfaces/flag-registry.interface';
|
|
2
5
|
export declare class TestFeatureFlagModule {
|
|
3
6
|
static register(flags?: Record<string, boolean>): DynamicModule;
|
|
7
|
+
static registerRegistry<TFlags extends FlagRegistry>(flags: TFlags, options?: TestFeatureFlagRegistryOptions<TFlags>): DynamicModule;
|
|
8
|
+
private static createModule;
|
|
9
|
+
}
|
|
10
|
+
export interface TestFeatureFlagRegistryOptions<TFlags extends FlagRegistry> {
|
|
11
|
+
overrides?: Partial<Record<FlagKey<TFlags>, boolean>>;
|
|
12
|
+
}
|
|
13
|
+
export declare class TestFeatureFlagController<TFlags extends FlagRegistry = FlagRegistry> {
|
|
14
|
+
private readonly defaults;
|
|
15
|
+
private values;
|
|
16
|
+
constructor(defaults: Record<string, boolean>, overrides?: Partial<Record<FlagKey<TFlags>, boolean>>);
|
|
17
|
+
set<K extends FlagKey<TFlags>>(key: K, value: boolean): void;
|
|
18
|
+
reset(): void;
|
|
19
|
+
isEnabled(key: string, options?: EvaluateBooleanOptions): boolean;
|
|
20
|
+
evaluateAll(): Record<string, boolean>;
|
|
21
|
+
getDetails(key: string, options?: EvaluateBooleanOptions): BooleanEvaluationDetails;
|
|
22
|
+
findFlag(key: string): FeatureFlagWithOverrides;
|
|
23
|
+
findAll(): FeatureFlagWithOverrides[];
|
|
24
|
+
private getValue;
|
|
4
25
|
}
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
};
|
|
8
8
|
var TestFeatureFlagModule_1;
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.TestFeatureFlagModule = void 0;
|
|
10
|
+
exports.TestFeatureFlagController = exports.TestFeatureFlagModule = void 0;
|
|
11
11
|
const common_1 = require("@nestjs/common");
|
|
12
12
|
const feature_flag_service_1 = require("../services/feature-flag.service");
|
|
13
13
|
function makeStubFlag(partial = {}) {
|
|
@@ -26,32 +26,29 @@ function makeStubFlag(partial = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
let TestFeatureFlagModule = TestFeatureFlagModule_1 = class TestFeatureFlagModule {
|
|
28
28
|
static register(flags) {
|
|
29
|
+
const controller = new TestFeatureFlagController(flags ?? {});
|
|
30
|
+
return this.createModule(controller);
|
|
31
|
+
}
|
|
32
|
+
static registerRegistry(flags, options = {}) {
|
|
33
|
+
const defaults = Object.fromEntries(Object.entries(flags).map(([key, definition]) => [key, definition.defaultValue]));
|
|
34
|
+
const controller = new TestFeatureFlagController(defaults, options.overrides);
|
|
35
|
+
return this.createModule(controller);
|
|
36
|
+
}
|
|
37
|
+
static createModule(controller) {
|
|
29
38
|
return {
|
|
30
39
|
module: TestFeatureFlagModule_1,
|
|
31
40
|
global: true,
|
|
32
41
|
providers: [
|
|
42
|
+
{
|
|
43
|
+
provide: TestFeatureFlagController,
|
|
44
|
+
useValue: controller,
|
|
45
|
+
},
|
|
33
46
|
{
|
|
34
47
|
provide: feature_flag_service_1.FeatureFlagService,
|
|
35
|
-
useValue:
|
|
36
|
-
isEnabled: async (key) => flags?.[key] ?? false,
|
|
37
|
-
evaluateAll: async () => flags ?? {},
|
|
38
|
-
create: async (input) => makeStubFlag({ key: input.key, enabled: input.enabled ?? false }),
|
|
39
|
-
update: async (key, input) => makeStubFlag({ key, ...input }),
|
|
40
|
-
archive: async (key) => makeStubFlag({ key, archivedAt: new Date() }),
|
|
41
|
-
setOverride: async () => { },
|
|
42
|
-
removeOverride: async () => { },
|
|
43
|
-
findAll: async () => Object.entries(flags ?? {}).map(([key, enabled]) => makeStubFlag({ key, enabled })),
|
|
44
|
-
findByKey: async (key) => {
|
|
45
|
-
if (flags && key in flags) {
|
|
46
|
-
return makeStubFlag({ key, enabled: flags[key] });
|
|
47
|
-
}
|
|
48
|
-
throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
|
|
49
|
-
},
|
|
50
|
-
invalidateCache: async () => { },
|
|
51
|
-
},
|
|
48
|
+
useValue: createFeatureFlagServiceStub(controller),
|
|
52
49
|
},
|
|
53
50
|
],
|
|
54
|
-
exports: [feature_flag_service_1.FeatureFlagService],
|
|
51
|
+
exports: [feature_flag_service_1.FeatureFlagService, TestFeatureFlagController],
|
|
55
52
|
};
|
|
56
53
|
}
|
|
57
54
|
};
|
|
@@ -59,4 +56,66 @@ exports.TestFeatureFlagModule = TestFeatureFlagModule;
|
|
|
59
56
|
exports.TestFeatureFlagModule = TestFeatureFlagModule = TestFeatureFlagModule_1 = __decorate([
|
|
60
57
|
(0, common_1.Module)({})
|
|
61
58
|
], TestFeatureFlagModule);
|
|
59
|
+
class TestFeatureFlagController {
|
|
60
|
+
constructor(defaults, overrides = {}) {
|
|
61
|
+
this.defaults = { ...defaults };
|
|
62
|
+
this.values = { ...defaults, ...overrides };
|
|
63
|
+
}
|
|
64
|
+
set(key, value) {
|
|
65
|
+
this.values[key] = value;
|
|
66
|
+
}
|
|
67
|
+
reset() {
|
|
68
|
+
this.values = { ...this.defaults };
|
|
69
|
+
}
|
|
70
|
+
isEnabled(key, options = {}) {
|
|
71
|
+
return this.getValue(key, options);
|
|
72
|
+
}
|
|
73
|
+
evaluateAll() {
|
|
74
|
+
return { ...this.values };
|
|
75
|
+
}
|
|
76
|
+
getDetails(key, options = {}) {
|
|
77
|
+
const hasValue = Object.prototype.hasOwnProperty.call(this.values, key);
|
|
78
|
+
const value = this.getValue(key, options);
|
|
79
|
+
return {
|
|
80
|
+
flagKey: key,
|
|
81
|
+
value,
|
|
82
|
+
result: value,
|
|
83
|
+
source: hasValue ? 'default' : 'default',
|
|
84
|
+
reason: hasValue ? 'GLOBAL' : 'FLAG_NOT_FOUND',
|
|
85
|
+
defaultUsed: !hasValue,
|
|
86
|
+
evaluationTimeMs: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
findFlag(key) {
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(this.values, key)) {
|
|
91
|
+
throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
|
|
92
|
+
}
|
|
93
|
+
return makeStubFlag({ key, enabled: this.values[key] });
|
|
94
|
+
}
|
|
95
|
+
findAll() {
|
|
96
|
+
return Object.entries(this.values).map(([key, enabled]) => makeStubFlag({ key, enabled }));
|
|
97
|
+
}
|
|
98
|
+
getValue(key, options) {
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(this.values, key)) {
|
|
100
|
+
return this.values[key];
|
|
101
|
+
}
|
|
102
|
+
return options.defaultValue ?? false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.TestFeatureFlagController = TestFeatureFlagController;
|
|
106
|
+
function createFeatureFlagServiceStub(controller) {
|
|
107
|
+
return {
|
|
108
|
+
isEnabled: async (key, _context, options = {}) => controller.isEnabled(key, options),
|
|
109
|
+
evaluateBoolean: async (key, _context, options = {}) => controller.getDetails(key, options),
|
|
110
|
+
evaluateAll: async () => controller.evaluateAll(),
|
|
111
|
+
create: async (input) => makeStubFlag({ key: input.key, enabled: input.enabled ?? false }),
|
|
112
|
+
update: async (key, input) => makeStubFlag({ key, ...input }),
|
|
113
|
+
archive: async (key) => makeStubFlag({ key, archivedAt: new Date() }),
|
|
114
|
+
setOverride: async () => { },
|
|
115
|
+
removeOverride: async () => { },
|
|
116
|
+
findAll: async () => controller.findAll(),
|
|
117
|
+
findByKey: async (key) => controller.findFlag(key),
|
|
118
|
+
invalidateCache: async () => { },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
62
121
|
//# sourceMappingURL=test-feature-flag.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-feature-flag.module.js","sourceRoot":"","sources":["../../src/testing/test-feature-flag.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAA0E;AAC1E,2EAAsE;
|
|
1
|
+
{"version":3,"file":"test-feature-flag.module.js","sourceRoot":"","sources":["../../src/testing/test-feature-flag.module.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TargetingAttributes, TargetingAttributeValue } from '../interfaces/feature-flag.interface';
|
|
2
|
+
interface AttributeValidationOptions {
|
|
3
|
+
allowEmpty: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function isTargetingAttributeValue(value: unknown): value is TargetingAttributeValue;
|
|
6
|
+
export declare function isTargetingAttributes(value: unknown, options?: AttributeValidationOptions): value is TargetingAttributes;
|
|
7
|
+
export declare function normalizeTargetingAttributes(value: unknown, options?: AttributeValidationOptions): TargetingAttributes;
|
|
8
|
+
export declare function matchesTargetingAttributes(overrideAttributes: TargetingAttributes, contextAttributes: TargetingAttributes): boolean;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTargetingAttributeValue = isTargetingAttributeValue;
|
|
4
|
+
exports.isTargetingAttributes = isTargetingAttributes;
|
|
5
|
+
exports.normalizeTargetingAttributes = normalizeTargetingAttributes;
|
|
6
|
+
exports.matchesTargetingAttributes = matchesTargetingAttributes;
|
|
7
|
+
const INVALID_ATTRIBUTES_MESSAGE = 'attributes must be a non-empty object with string, number, boolean, or null values';
|
|
8
|
+
function isTargetingAttributeValue(value) {
|
|
9
|
+
return (value === null ||
|
|
10
|
+
typeof value === 'string' ||
|
|
11
|
+
(typeof value === 'number' && Number.isFinite(value)) ||
|
|
12
|
+
typeof value === 'boolean');
|
|
13
|
+
}
|
|
14
|
+
function isTargetingAttributes(value, options = { allowEmpty: false }) {
|
|
15
|
+
if (!isPlainObject(value)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const entries = Object.entries(value);
|
|
19
|
+
if (!options.allowEmpty && entries.length === 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return entries.every(([, attributeValue]) => isTargetingAttributeValue(attributeValue));
|
|
23
|
+
}
|
|
24
|
+
function normalizeTargetingAttributes(value, options = { allowEmpty: false }) {
|
|
25
|
+
if (!isTargetingAttributes(value, options)) {
|
|
26
|
+
throw new Error(INVALID_ATTRIBUTES_MESSAGE);
|
|
27
|
+
}
|
|
28
|
+
return { ...value };
|
|
29
|
+
}
|
|
30
|
+
function matchesTargetingAttributes(overrideAttributes, contextAttributes) {
|
|
31
|
+
const entries = Object.entries(overrideAttributes);
|
|
32
|
+
if (entries.length === 0) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return entries.every(([key, value]) => contextAttributes[key] === value);
|
|
36
|
+
}
|
|
37
|
+
function isPlainObject(value) {
|
|
38
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const prototype = Object.getPrototypeOf(value);
|
|
42
|
+
return prototype === Object.prototype || prototype === null;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=targeting-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targeting-attributes.js","sourceRoot":"","sources":["../../src/utils/targeting-attributes.ts"],"names":[],"mappings":";;AAYA,8DAOC;AAED,sDAcC;AAED,oEASC;AAED,gEAUC;AAjDD,MAAM,0BAA0B,GAC9B,oFAAoF,CAAC;AAEvF,SAAgB,yBAAyB,CAAC,KAAc;IACtD,OAAO,CACL,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrD,OAAO,KAAK,KAAK,SAAS,CAC3B,CAAC;AACJ,CAAC;AAED,SAAgB,qBAAqB,CACnC,KAAc,EACd,UAAsC,EAAE,UAAU,EAAE,KAAK,EAAE;IAE3D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,yBAAyB,CAAC,cAAc,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,SAAgB,4BAA4B,CAC1C,KAAc,EACd,UAAsC,EAAE,UAAU,EAAE,KAAK,EAAE;IAE3D,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AACtB,CAAC;AAED,SAAgB,0BAA0B,CACxC,kBAAuC,EACvC,iBAAsC;IAEtC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestarc/feature-flag",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DB-backed feature flags for NestJS + Prisma + PostgreSQL with tenant-aware overrides",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"./testing": {
|
|
13
13
|
"types": "./dist/testing/index.d.ts",
|
|
14
14
|
"default": "./dist/testing/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./openfeature": {
|
|
17
|
+
"types": "./dist/openfeature.d.ts",
|
|
18
|
+
"default": "./dist/openfeature.js"
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
"files": [
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
},
|
|
36
40
|
"repository": {
|
|
37
41
|
"type": "git",
|
|
38
|
-
"url": "https://github.com/nestarc/nestjs-feature-flag"
|
|
42
|
+
"url": "git+https://github.com/nestarc/nestjs-feature-flag.git"
|
|
39
43
|
},
|
|
40
44
|
"keywords": [
|
|
41
45
|
"nestjs",
|
|
@@ -55,7 +59,10 @@
|
|
|
55
59
|
"peerDependencies": {
|
|
56
60
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
57
61
|
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
62
|
+
"@openfeature/server-sdk": "^1.0.0",
|
|
58
63
|
"@prisma/client": "^5.0.0 || ^6.0.0",
|
|
64
|
+
"class-transformer": "^0.5.1",
|
|
65
|
+
"class-validator": "^0.14.0 || ^0.15.0",
|
|
59
66
|
"ioredis": "^5.0.0",
|
|
60
67
|
"reflect-metadata": "^0.1.13 || ^0.2.0",
|
|
61
68
|
"rxjs": "^7.0.0"
|
|
@@ -67,10 +74,17 @@
|
|
|
67
74
|
"@nestjs/event-emitter": {
|
|
68
75
|
"optional": true
|
|
69
76
|
},
|
|
77
|
+
"@openfeature/server-sdk": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
70
80
|
"ioredis": {
|
|
71
81
|
"optional": true
|
|
72
82
|
}
|
|
73
83
|
},
|
|
84
|
+
"overrides": {
|
|
85
|
+
"js-yaml": "4.2.0",
|
|
86
|
+
"multer": "2.2.0"
|
|
87
|
+
},
|
|
74
88
|
"devDependencies": {
|
|
75
89
|
"@eslint/js": "^9.0.0",
|
|
76
90
|
"@nestjs/common": "^11.0.0",
|
|
@@ -83,6 +97,8 @@
|
|
|
83
97
|
"@types/ioredis-mock": "^8.2.7",
|
|
84
98
|
"@types/jest": "^29.5.0",
|
|
85
99
|
"@types/supertest": "^7.2.0",
|
|
100
|
+
"class-transformer": "^0.5.1",
|
|
101
|
+
"class-validator": "^0.15.1",
|
|
86
102
|
"dotenv-cli": "^11.0.0",
|
|
87
103
|
"eslint": "^9.0.0",
|
|
88
104
|
"ioredis": "^5.10.1",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
ALTER TABLE "feature_flag_overrides"
|
|
2
|
+
ADD COLUMN "attributes" JSONB NOT NULL DEFAULT '{}',
|
|
3
|
+
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
|
|
4
|
+
|
|
5
|
+
ALTER TABLE "feature_flag_overrides"
|
|
6
|
+
ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
|
7
|
+
|
|
8
|
+
UPDATE "feature_flag_overrides"
|
|
9
|
+
SET "attributes" = jsonb_strip_nulls(
|
|
10
|
+
jsonb_build_object(
|
|
11
|
+
'tenantId', "tenant_id",
|
|
12
|
+
'userId', "user_id",
|
|
13
|
+
'environment', "environment"
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- v0.3.0 rejects empty targeting attributes. Legacy all-null/global overrides
|
|
18
|
+
-- become empty objects during backfill and are intentionally removed by this
|
|
19
|
+
-- breaking migration.
|
|
20
|
+
DELETE FROM "feature_flag_overrides"
|
|
21
|
+
WHERE "attributes" = '{}'::jsonb;
|
|
22
|
+
|
|
23
|
+
WITH ranked_overrides AS (
|
|
24
|
+
SELECT
|
|
25
|
+
"id",
|
|
26
|
+
row_number() OVER (
|
|
27
|
+
PARTITION BY "flag_id", "attributes"
|
|
28
|
+
ORDER BY "updated_at" DESC, "created_at" DESC, "id" DESC
|
|
29
|
+
) AS row_number
|
|
30
|
+
FROM "feature_flag_overrides"
|
|
31
|
+
)
|
|
32
|
+
DELETE FROM "feature_flag_overrides" target
|
|
33
|
+
USING ranked_overrides ranked
|
|
34
|
+
WHERE target."id" = ranked."id"
|
|
35
|
+
AND ranked.row_number > 1;
|
|
36
|
+
|
|
37
|
+
DROP INDEX IF EXISTS "uq_override_000";
|
|
38
|
+
DROP INDEX IF EXISTS "uq_override_001";
|
|
39
|
+
DROP INDEX IF EXISTS "uq_override_010";
|
|
40
|
+
DROP INDEX IF EXISTS "uq_override_011";
|
|
41
|
+
DROP INDEX IF EXISTS "uq_override_100";
|
|
42
|
+
DROP INDEX IF EXISTS "uq_override_101";
|
|
43
|
+
DROP INDEX IF EXISTS "uq_override_110";
|
|
44
|
+
DROP INDEX IF EXISTS "uq_override_111";
|
|
45
|
+
|
|
46
|
+
ALTER TABLE "feature_flag_overrides"
|
|
47
|
+
DROP COLUMN "tenant_id",
|
|
48
|
+
DROP COLUMN "user_id",
|
|
49
|
+
DROP COLUMN "environment";
|
|
50
|
+
|
|
51
|
+
ALTER TABLE "feature_flag_overrides"
|
|
52
|
+
ALTER COLUMN "attributes" DROP DEFAULT;
|
|
53
|
+
|
|
54
|
+
CREATE UNIQUE INDEX "uq_feature_flag_override_attributes"
|
|
55
|
+
ON "feature_flag_overrides"("flag_id", "attributes");
|
|
56
|
+
|
|
57
|
+
ALTER TABLE "feature_flag_overrides"
|
|
58
|
+
ADD CONSTRAINT "chk_feature_flag_override_attributes_non_empty"
|
|
59
|
+
CHECK (jsonb_typeof("attributes") = 'object' AND "attributes" <> '{}'::jsonb);
|
package/prisma/schema.prisma
CHANGED
|
@@ -24,18 +24,16 @@ model FeatureFlag {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
model FeatureFlagOverride {
|
|
27
|
-
id
|
|
28
|
-
flagId
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
|
27
|
+
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
|
28
|
+
flagId String @map("flag_id") @db.Uuid
|
|
29
|
+
attributes Json
|
|
30
|
+
priority Int @default(0)
|
|
31
|
+
enabled Boolean
|
|
32
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
|
33
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
|
35
34
|
|
|
36
35
|
flag FeatureFlag @relation(fields: [flagId], references: [id], onDelete: Cascade)
|
|
37
36
|
|
|
38
|
-
// Unique indexes defined in raw SQL migration (0001) to handle NULL equality correctly
|
|
39
37
|
@@index([flagId], map: "idx_override_flag_id")
|
|
40
38
|
@@map("feature_flag_overrides")
|
|
41
39
|
}
|