@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.
Files changed (57) hide show
  1. package/README.md +206 -81
  2. package/dist/admin/feature-flag-admin.controller.d.ts +8 -6
  3. package/dist/admin/feature-flag-admin.controller.js +22 -4
  4. package/dist/admin/feature-flag-admin.controller.js.map +1 -1
  5. package/dist/admin/feature-flag-admin.dto.d.ts +30 -0
  6. package/dist/admin/feature-flag-admin.dto.js +116 -0
  7. package/dist/admin/feature-flag-admin.dto.js.map +1 -0
  8. package/dist/admin/targeting-attributes.validator.d.ts +6 -0
  9. package/dist/admin/targeting-attributes.validator.js +36 -0
  10. package/dist/admin/targeting-attributes.validator.js.map +1 -0
  11. package/dist/events/feature-flag.events.d.ts +38 -6
  12. package/dist/events/feature-flag.events.js +1 -0
  13. package/dist/events/feature-flag.events.js.map +1 -1
  14. package/dist/flag-registry.d.ts +24 -0
  15. package/dist/flag-registry.js +62 -0
  16. package/dist/flag-registry.js.map +1 -0
  17. package/dist/guards/feature-flag.guard.js +5 -1
  18. package/dist/guards/feature-flag.guard.js.map +1 -1
  19. package/dist/index.d.ts +6 -3
  20. package/dist/index.js +7 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/interfaces/evaluation-context.interface.d.ts +8 -3
  23. package/dist/interfaces/evaluation-details.interface.d.ts +29 -0
  24. package/dist/interfaces/evaluation-details.interface.js +3 -0
  25. package/dist/interfaces/evaluation-details.interface.js.map +1 -0
  26. package/dist/interfaces/feature-flag-options.interface.d.ts +3 -0
  27. package/dist/interfaces/feature-flag-repository.interface.d.ts +8 -6
  28. package/dist/interfaces/feature-flag.interface.d.ts +18 -9
  29. package/dist/interfaces/flag-registry.interface.d.ts +17 -0
  30. package/dist/interfaces/flag-registry.interface.js +3 -0
  31. package/dist/interfaces/flag-registry.interface.js.map +1 -0
  32. package/dist/openfeature.d.ts +26 -0
  33. package/dist/openfeature.js +96 -0
  34. package/dist/openfeature.js.map +1 -0
  35. package/dist/repositories/prisma-feature-flag.repository.d.ts +3 -3
  36. package/dist/repositories/prisma-feature-flag.repository.js +18 -14
  37. package/dist/repositories/prisma-feature-flag.repository.js.map +1 -1
  38. package/dist/services/feature-flag.service.d.ts +15 -7
  39. package/dist/services/feature-flag.service.js +126 -38
  40. package/dist/services/feature-flag.service.js.map +1 -1
  41. package/dist/services/flag-context-resolver.js +14 -3
  42. package/dist/services/flag-context-resolver.js.map +1 -1
  43. package/dist/services/flag-evaluator.service.d.ts +8 -8
  44. package/dist/services/flag-evaluator.service.js +90 -36
  45. package/dist/services/flag-evaluator.service.js.map +1 -1
  46. package/dist/testing/index.d.ts +1 -1
  47. package/dist/testing/index.js +2 -1
  48. package/dist/testing/index.js.map +1 -1
  49. package/dist/testing/test-feature-flag.module.d.ts +21 -0
  50. package/dist/testing/test-feature-flag.module.js +78 -19
  51. package/dist/testing/test-feature-flag.module.js.map +1 -1
  52. package/dist/utils/targeting-attributes.d.ts +9 -0
  53. package/dist/utils/targeting-attributes.js +44 -0
  54. package/dist/utils/targeting-attributes.js.map +1 -0
  55. package/package.json +18 -2
  56. package/prisma/migrations/20260512000000_attribute_targeting/migration.sql +59 -0
  57. 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 { result: false, source: 'global' };
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
- // 3. Tenant override
28
- if (context.tenantId) {
29
- const tenantOverride = flag.overrides.find((o) => o.tenantId === context.tenantId &&
30
- o.userId === null &&
31
- (o.environment === null || o.environment === context.environment));
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 { result: true, source: 'percentage' };
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 hashKey = context.userId ?? context.tenantId ?? '';
49
- if (!hashKey) {
50
- return { result: flag.enabled, source: 'global' };
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
- // 6. Global default
56
- return { result: flag.enabled, source: 'global' };
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":"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"}
1
+ {"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- export { TestFeatureFlagModule } from './test-feature-flag.module';
1
+ export { TestFeatureFlagController, TestFeatureFlagModule, TestFeatureFlagRegistryOptions, } from './test-feature-flag.module';
@@ -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,uEAAmE;AAA1D,iIAAA,qBAAqB,OAAA"}
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;AAMtE,SAAS,YAAY,CACnB,UAA6C,EAAE;IAE/C,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,MAAM;QAC1B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;QACxC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK;QACjC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,CAAC;QACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;QAChC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,IAAI;QACtC,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC;QACtD,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC;QACtD,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;KACnC,CAAC;AACJ,CAAC;AAGM,IAAM,qBAAqB,6BAA3B,MAAM,qBAAqB;IAChC,MAAM,CAAC,QAAQ,CAAC,KAA+B;QAC7C,OAAO;YACL,MAAM,EAAE,uBAAqB;YAC7B,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,yCAAkB;oBAC3B,QAAQ,EAAE;wBACR,SAAS,EAAE,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK;wBACvD,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE;wBACpC,MAAM,EAAE,KAAK,EAAE,KAA6B,EAAE,EAAE,CAC9C,YAAY,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK,EAAE,CAAC;wBACnE,MAAM,EAAE,KAAK,EAAE,GAAW,EAAE,KAAwC,EAAE,EAAE,CACtE,YAAY,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;wBACjC,OAAO,EAAE,KAAK,EAAE,GAAW,EAAE,EAAE,CAC7B,YAAY,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;wBAC/C,WAAW,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;wBAC3B,cAAc,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;wBAC9B,OAAO,EAAE,KAAK,IAAI,EAAE,CAClB,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,CACjD,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAC/B;wBACH,SAAS,EAAE,KAAK,EAAE,GAAW,EAAE,EAAE;4BAC/B,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;gCAC1B,OAAO,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;4BACpD,CAAC;4BACD,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;wBACjE,CAAC;wBACD,eAAe,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;qBAChC;iBACF;aACF;YACD,OAAO,EAAE,CAAC,yCAAkB,CAAC;SAC9B,CAAC;IACJ,CAAC;CACF,CAAA;AApCY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,qBAAqB,CAoCjC"}
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.2.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);
@@ -24,18 +24,16 @@ model FeatureFlag {
24
24
  }
25
25
 
26
26
  model FeatureFlagOverride {
27
- id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
28
- flagId String @map("flag_id") @db.Uuid
29
- tenantId String? @map("tenant_id")
30
- userId String? @map("user_id")
31
- environment String?
32
- enabled Boolean
33
- createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
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
  }