flagsmith-nodejs 6.1.0 → 7.0.2

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 (117) hide show
  1. package/.github/workflows/conventional-commit.yml +29 -0
  2. package/.github/workflows/publish.yml +20 -17
  3. package/.github/workflows/pull_request.yaml +36 -33
  4. package/.github/workflows/release-please.yml +18 -0
  5. package/.gitmodules +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.cjs +9 -1
  9. package/.release-please-manifest.json +1 -0
  10. package/CHANGELOG.md +592 -0
  11. package/CODEOWNERS +1 -0
  12. package/README.md +0 -2
  13. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  14. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  15. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  19. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  20. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  21. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  22. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  23. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  24. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  25. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  26. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  27. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  28. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  29. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  30. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  31. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  32. package/build/cjs/flagsmith-engine/index.js +176 -56
  33. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  34. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  35. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  36. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  37. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  38. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  39. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  40. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  41. package/build/cjs/sdk/analytics.js +3 -1
  42. package/build/cjs/sdk/index.d.ts +1 -3
  43. package/build/cjs/sdk/index.js +63 -24
  44. package/build/cjs/sdk/models.d.ts +8 -1
  45. package/build/cjs/sdk/models.js +29 -1
  46. package/build/cjs/sdk/utils.d.ts +1 -0
  47. package/build/cjs/sdk/utils.js +14 -1
  48. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  49. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  50. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  51. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  52. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  53. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  54. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  55. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  56. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  57. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  58. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  59. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  60. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  61. package/build/esm/flagsmith-engine/features/models.js +2 -2
  62. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  63. package/build/esm/flagsmith-engine/features/types.js +6 -0
  64. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  65. package/build/esm/flagsmith-engine/features/util.js +3 -0
  66. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  67. package/build/esm/flagsmith-engine/index.js +161 -43
  68. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  69. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  70. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  71. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  72. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  73. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  74. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  75. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  76. package/build/esm/sdk/analytics.js +3 -1
  77. package/build/esm/sdk/index.d.ts +1 -3
  78. package/build/esm/sdk/index.js +63 -24
  79. package/build/esm/sdk/models.d.ts +8 -1
  80. package/build/esm/sdk/models.js +29 -1
  81. package/build/esm/sdk/utils.d.ts +1 -0
  82. package/build/esm/sdk/utils.js +12 -0
  83. package/flagsmith-engine/environments/models.ts +3 -1
  84. package/flagsmith-engine/environments/util.ts +2 -1
  85. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  86. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  87. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  88. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  89. package/flagsmith-engine/evaluation/models.ts +96 -0
  90. package/flagsmith-engine/features/models.ts +3 -2
  91. package/flagsmith-engine/features/types.ts +5 -0
  92. package/flagsmith-engine/features/util.ts +4 -0
  93. package/flagsmith-engine/index.ts +229 -72
  94. package/flagsmith-engine/segments/constants.ts +1 -0
  95. package/flagsmith-engine/segments/evaluators.ts +178 -62
  96. package/flagsmith-engine/segments/models.ts +171 -23
  97. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  98. package/package.json +13 -2
  99. package/release-please-config.json +62 -0
  100. package/sdk/analytics.ts +3 -1
  101. package/sdk/index.ts +89 -30
  102. package/sdk/models.ts +44 -2
  103. package/sdk/utils.ts +13 -0
  104. package/tests/engine/e2e/engine.test.ts +43 -38
  105. package/tests/engine/unit/engine.test.ts +306 -60
  106. package/tests/engine/unit/mappers.test.ts +353 -0
  107. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  108. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  109. package/tests/engine/unit/utils/utils.test.ts +7 -7
  110. package/tests/engine/unit/utils.ts +1 -1
  111. package/tests/sdk/analytics.test.ts +6 -1
  112. package/tests/sdk/data/environment.json +1 -0
  113. package/tests/sdk/flagsmith-environment-flags.test.ts +28 -0
  114. package/tests/sdk/flagsmith-identity-flags.test.ts +11 -2
  115. package/tests/sdk/flagsmith.test.ts +190 -3
  116. package/tests/sdk/offline-handlers.test.ts +3 -1
  117. package/vitest.config.esm.ts +34 -0
@@ -9,6 +9,7 @@ import {
9
9
  badFetch
10
10
  } from './utils.js';
11
11
  import { DefaultFlag } from '../../sdk/models.js';
12
+ import { getUserAgent } from '../../sdk/utils.js';
12
13
 
13
14
  vi.mock('../../sdk/polling_manager');
14
15
 
@@ -150,7 +151,11 @@ test('test_transient_identity', async () => {
150
151
  `https://edge.api.flagsmith.com/api/v1/identities/`,
151
152
  expect.objectContaining({
152
153
  method: 'POST',
153
- headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'sometestfakekey' },
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'X-Environment-Key': 'sometestfakekey',
157
+ 'User-Agent': getUserAgent()
158
+ },
154
159
  body: JSON.stringify({ identifier, traits: traitsInRequest, transient })
155
160
  })
156
161
  );
@@ -191,7 +196,11 @@ test('test_identity_with_transient_traits', async () => {
191
196
  `https://edge.api.flagsmith.com/api/v1/identities/`,
192
197
  expect.objectContaining({
193
198
  method: 'POST',
194
- headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'sometestfakekey' },
199
+ headers: {
200
+ 'Content-Type': 'application/json',
201
+ 'X-Environment-Key': 'sometestfakekey',
202
+ 'User-Agent': getUserAgent()
203
+ },
195
204
  body: JSON.stringify({ identifier, traits: traitsInRequest })
196
205
  })
197
206
  );
@@ -9,13 +9,17 @@ import {
9
9
  badFetch
10
10
  } from './utils.js';
11
11
  import { DefaultFlag, Flags } from '../../sdk/models.js';
12
- import { delay } from '../../sdk/utils.js';
12
+ import { delay, getUserAgent } from '../../sdk/utils.js';
13
13
  import { EnvironmentModel } from '../../flagsmith-engine/environments/models.js';
14
14
  import { BaseOfflineHandler } from '../../sdk/offline_handlers.js';
15
15
  import { Agent } from 'undici';
16
16
 
17
+ const isEsmBuild = process.env.ESM_BUILD === 'true';
18
+
17
19
  vi.mock('../../sdk/polling_manager');
18
- test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
20
+
21
+ // Skip in ESM build: vi.mock doesn't work with external modules
22
+ test.skipIf(isEsmBuild)('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
19
23
  new Flagsmith({
20
24
  environmentKey: 'ser.key',
21
25
  enableLocalEvaluation: true
@@ -32,7 +36,8 @@ test('test_flagsmith_local_evaluation_key_required', () => {
32
36
  }).toThrow('Using local evaluation requires a server-side environment key');
33
37
  });
34
38
 
35
- test('test_update_environment_sets_environment', async () => {
39
+ // Skip in ESM build: instanceof fails across module boundaries
40
+ test.skipIf(isEsmBuild)('test_update_environment_sets_environment', async () => {
36
41
  const flg = flagsmith({
37
42
  environmentKey: 'ser.key'
38
43
  });
@@ -40,6 +45,160 @@ test('test_update_environment_sets_environment', async () => {
40
45
  expect(await flg.getEnvironment()).toStrictEqual(model);
41
46
  });
42
47
 
48
+ test('test_update_environment_handles_paginated_document', async () => {
49
+ type EnvDocumentMockResponse = {
50
+ responseHeader: string | null;
51
+ page: any;
52
+ };
53
+
54
+ const createMockFetch = (pages: EnvDocumentMockResponse[]) => {
55
+ let callCount = 0;
56
+ return vi.fn((url: string, options?: RequestInit) => {
57
+ if (url.includes('/environment-document')) {
58
+ const document = envDocumentMockResponse[callCount];
59
+ if (document) {
60
+ callCount++;
61
+
62
+ const responseHeaders: Record<string, string> = {};
63
+
64
+ if (document.responseHeader) {
65
+ responseHeaders['Link'] = `<${document.responseHeader}>; rel="next"`;
66
+ }
67
+
68
+ return Promise.resolve(
69
+ new Response(JSON.stringify(document.page), {
70
+ status: 200,
71
+ headers: responseHeaders
72
+ })
73
+ );
74
+ }
75
+ }
76
+ return Promise.resolve(new Response('unknown url ' + url, { status: 404 }));
77
+ });
78
+ };
79
+
80
+ const envDocumentMockResponse: EnvDocumentMockResponse[] = [
81
+ {
82
+ responseHeader: '/api/v1/environment-document?page=2',
83
+ page: {
84
+ id: 1,
85
+ api_key: 'test-key',
86
+ project: {
87
+ id: 1,
88
+ name: 'test',
89
+ organisation: {
90
+ id: 1,
91
+ name: 'Test Org',
92
+ feature_analytics: false,
93
+ persist_trait_data: true,
94
+ stop_serving_flags: false
95
+ },
96
+ hide_disabled_flags: false,
97
+ segments: []
98
+ },
99
+ feature_states: [
100
+ {
101
+ feature_state_value: 'first_page_feature_state',
102
+ multivariate_feature_state_values: [],
103
+ django_id: 81027,
104
+ feature: {
105
+ id: 15058,
106
+ type: 'STANDARD',
107
+ name: 'string_feature'
108
+ },
109
+ enabled: false
110
+ },
111
+ {
112
+ feature_state_value: 'second_page_feature_state',
113
+ multivariate_feature_state_values: [],
114
+ django_id: 81027,
115
+ feature: {
116
+ id: 15058,
117
+ type: 'STANDARD',
118
+ name: 'string_feature'
119
+ },
120
+ enabled: false
121
+ },
122
+ {
123
+ feature_state_value: 'third_page_feature_state',
124
+ multivariate_feature_state_values: [],
125
+ django_id: 81027,
126
+ feature: {
127
+ id: 15058,
128
+ type: 'STANDARD',
129
+ name: 'string_feature'
130
+ },
131
+ enabled: false
132
+ }
133
+ ],
134
+ identity_overrides: [{ id: 1, identifier: 'user1' }]
135
+ }
136
+ },
137
+ {
138
+ responseHeader: '/api/v1/environment-document?page=3',
139
+ page: {
140
+ api_key: 'test-key',
141
+ project: {
142
+ id: 1,
143
+ name: 'test',
144
+ organisation: {
145
+ id: 1,
146
+ name: 'Test Org',
147
+ feature_analytics: false,
148
+ persist_trait_data: true,
149
+ stop_serving_flags: false
150
+ },
151
+ hide_disabled_flags: false,
152
+ segments: []
153
+ },
154
+ feature_states: [],
155
+ identity_overrides: [{ id: 2, identifier: 'user2' }]
156
+ }
157
+ },
158
+ {
159
+ responseHeader: null,
160
+ page: {
161
+ api_key: 'test-key',
162
+ project: {
163
+ id: 1,
164
+ name: 'test',
165
+ organisation: {
166
+ id: 1,
167
+ name: 'Test Org',
168
+ feature_analytics: false,
169
+ persist_trait_data: true,
170
+ stop_serving_flags: false
171
+ },
172
+ hide_disabled_flags: false,
173
+ segments: []
174
+ },
175
+ feature_states: [],
176
+ identity_overrides: [{ id: 2, identifier: 'user3' }]
177
+ }
178
+ }
179
+ ];
180
+
181
+ const flg = new Flagsmith({
182
+ environmentKey: 'ser.key',
183
+ enableLocalEvaluation: true,
184
+ fetch: createMockFetch(envDocumentMockResponse)
185
+ });
186
+
187
+ const environment = await flg.getEnvironment();
188
+
189
+ expect(environment.identityOverrides).toHaveLength(3);
190
+ expect(environment.identityOverrides[0].identifier).toBe('user1');
191
+ expect(environment.identityOverrides[1].identifier).toBe('user2');
192
+ expect(environment.identityOverrides[2].identifier).toBe('user3');
193
+ expect(environment.featureStates).toHaveLength(3);
194
+ expect(environment.featureStates[0].getValue()).toBe('first_page_feature_state');
195
+ expect(environment.featureStates[1].getValue()).toBe('second_page_feature_state');
196
+ expect(environment.featureStates[2].getValue()).toBe('third_page_feature_state');
197
+ expect(environment.project.name).toBe('test');
198
+ expect(environment.project.organisation.name).toBe('Test Org');
199
+ expect(environment.project.organisation.id).toBe(1);
200
+ });
201
+
43
202
  test('test_set_agent_options', async () => {
44
203
  const agent = new Agent({});
45
204
 
@@ -358,3 +517,31 @@ test('getIdentityFlags succeeds if initial fetch failed then succeeded', async (
358
517
  const flags2 = await flg.getIdentityFlags('test-user');
359
518
  expect(flags2.isFeatureEnabled('some_feature')).toBe(true);
360
519
  });
520
+
521
+ // Skip in ESM build: require() path resolution differs
522
+ test.skipIf(isEsmBuild)('get_user_agent_extracts_version_from_package_json', async () => {
523
+ const userAgent = getUserAgent();
524
+ const packageJson = require('../../package.json');
525
+
526
+ expect(userAgent).toBe(`flagsmith-nodejs-sdk/${packageJson.version}`);
527
+ });
528
+
529
+ test('Flags.fromEvaluationResult throws error when metadata.id is missing', () => {
530
+ const evaluationResult = {
531
+ flags: {
532
+ test_feature: {
533
+ enabled: true,
534
+ name: 'test_feature',
535
+ value: 'test_value',
536
+ reason: 'DEFAULT',
537
+ metadata: {}
538
+ }
539
+ },
540
+ segments: []
541
+ };
542
+
543
+ expect(() => Flags.fromEvaluationResult(evaluationResult as any)).toThrow(
544
+ 'FlagResult metadata.id is missing for feature "test_feature". ' +
545
+ 'This indicates a bug in the SDK, please report it.'
546
+ );
547
+ });
@@ -7,8 +7,10 @@ import * as offlineEnvironment from './data/offline-environment.json';
7
7
  vi.mock('fs');
8
8
 
9
9
  const offlineEnvironmentString = JSON.stringify(offlineEnvironment);
10
+ const isEsmBuild = process.env.ESM_BUILD === 'true';
10
11
 
11
- test('local file handler', () => {
12
+ // Skip in ESM build: instanceof fails across module boundaries
13
+ test.skipIf(isEsmBuild)('local file handler', () => {
12
14
  const environmentDocumentFilePath = '/some/path/environment.json';
13
15
 
14
16
  // Mock the fs.readFileSync function to return environmentJson
@@ -0,0 +1,34 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Vitest config for testing against the built ESM output.
6
+ * This catches CJS/ESM interop issues (like jsonpath) that don't surface
7
+ * when testing TypeScript source directly.
8
+ *
9
+ * Run with: npm run test:esm-build (after npm run build)
10
+ */
11
+ export default defineConfig({
12
+ test: {
13
+ globals: true,
14
+ restoreMocks: true,
15
+ exclude: ['**/node_modules/**'],
16
+ server: {
17
+ deps: {
18
+ // Don't transform built ESM - test it as-is
19
+ external: [/build\/esm/]
20
+ }
21
+ }
22
+ },
23
+ resolve: {
24
+ alias: {
25
+ // Redirect source imports to built ESM output
26
+ '../../../flagsmith-engine': path.resolve(__dirname, 'build/esm/flagsmith-engine'),
27
+ '../../../../flagsmith-engine': path.resolve(__dirname, 'build/esm/flagsmith-engine'),
28
+ '../../../sdk': path.resolve(__dirname, 'build/esm/sdk'),
29
+ '../../../../sdk': path.resolve(__dirname, 'build/esm/sdk'),
30
+ '../../sdk': path.resolve(__dirname, 'build/esm/sdk'),
31
+ '../sdk': path.resolve(__dirname, 'build/esm/sdk')
32
+ }
33
+ }
34
+ });