@depup/launchdarkly-node-server-sdk 7.0.4-depup.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 (107) hide show
  1. package/.babelrc +16 -0
  2. package/.circleci/config.yml +89 -0
  3. package/.eslintignore +5 -0
  4. package/.eslintrc.yaml +114 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/stale.yml +8 -0
  10. package/.hound.yml +33 -0
  11. package/.ldrelease/config.yml +28 -0
  12. package/.prettierrc +6 -0
  13. package/CHANGELOG.md +603 -0
  14. package/CODEOWNERS +2 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/LICENSE.txt +13 -0
  17. package/README.md +36 -0
  18. package/SECURITY.md +5 -0
  19. package/attribute_reference.js +217 -0
  20. package/big_segments.js +117 -0
  21. package/caching_store_wrapper.js +240 -0
  22. package/changes.json +30 -0
  23. package/configuration.js +235 -0
  24. package/context.js +98 -0
  25. package/context_filter.js +137 -0
  26. package/contract-tests/README.md +7 -0
  27. package/contract-tests/index.js +109 -0
  28. package/contract-tests/log.js +23 -0
  29. package/contract-tests/package.json +15 -0
  30. package/contract-tests/sdkClientEntity.js +110 -0
  31. package/contract-tests/testharness-suppressions.txt +2 -0
  32. package/diagnostic_events.js +151 -0
  33. package/docs/typedoc.js +10 -0
  34. package/errors.js +26 -0
  35. package/evaluator.js +822 -0
  36. package/event_factory.js +121 -0
  37. package/event_processor.js +320 -0
  38. package/event_summarizer.js +101 -0
  39. package/feature_store.js +120 -0
  40. package/feature_store_event_wrapper.js +258 -0
  41. package/file_data_source.js +192 -0
  42. package/flags_state.js +46 -0
  43. package/index.d.ts +2426 -0
  44. package/index.js +452 -0
  45. package/integrations.js +7 -0
  46. package/interfaces.js +2 -0
  47. package/loggers.js +125 -0
  48. package/messages.js +31 -0
  49. package/operators.js +106 -0
  50. package/package.json +105 -0
  51. package/polling.js +70 -0
  52. package/requestor.js +62 -0
  53. package/scripts/better-audit.sh +76 -0
  54. package/sharedtest/big_segment_store_tests.js +86 -0
  55. package/sharedtest/feature_store_tests.js +177 -0
  56. package/sharedtest/persistent_feature_store_tests.js +183 -0
  57. package/sharedtest/store_tests.js +7 -0
  58. package/streaming.js +179 -0
  59. package/test/LDClient-big-segments-test.js +92 -0
  60. package/test/LDClient-end-to-end-test.js +218 -0
  61. package/test/LDClient-evaluation-all-flags-test.js +226 -0
  62. package/test/LDClient-evaluation-test.js +204 -0
  63. package/test/LDClient-events-test.js +502 -0
  64. package/test/LDClient-listeners-test.js +180 -0
  65. package/test/LDClient-test.js +96 -0
  66. package/test/LDClient-tls-test.js +110 -0
  67. package/test/attribute_reference-test.js +494 -0
  68. package/test/big_segments-test.js +182 -0
  69. package/test/caching_store_wrapper-test.js +434 -0
  70. package/test/configuration-test.js +249 -0
  71. package/test/context-test.js +93 -0
  72. package/test/context_filter-test.js +424 -0
  73. package/test/diagnostic_events-test.js +152 -0
  74. package/test/evaluator-big-segments-test.js +301 -0
  75. package/test/evaluator-bucketing-test.js +333 -0
  76. package/test/evaluator-clause-test.js +277 -0
  77. package/test/evaluator-flag-test.js +452 -0
  78. package/test/evaluator-pre-conditions-test.js +105 -0
  79. package/test/evaluator-rule-test.js +131 -0
  80. package/test/evaluator-segment-match-test.js +310 -0
  81. package/test/evaluator_helpers.js +106 -0
  82. package/test/event_processor-test.js +680 -0
  83. package/test/event_summarizer-test.js +146 -0
  84. package/test/feature_store-test.js +42 -0
  85. package/test/feature_store_event_wrapper-test.js +182 -0
  86. package/test/feature_store_test_base.js +60 -0
  87. package/test/file_data_source-test.js +255 -0
  88. package/test/loggers-test.js +126 -0
  89. package/test/operators-test.js +102 -0
  90. package/test/polling-test.js +158 -0
  91. package/test/requestor-test.js +60 -0
  92. package/test/store_tests_big_segments-test.js +61 -0
  93. package/test/streaming-test.js +323 -0
  94. package/test/stubs.js +107 -0
  95. package/test/test_data-test.js +341 -0
  96. package/test/update_queue-test.js +61 -0
  97. package/test-types.ts +210 -0
  98. package/test_data.js +323 -0
  99. package/tsconfig.json +14 -0
  100. package/update_queue.js +28 -0
  101. package/utils/__tests__/httpUtils-test.js +39 -0
  102. package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
  103. package/utils/asyncUtils.js +32 -0
  104. package/utils/httpUtils.js +105 -0
  105. package/utils/stringifyAttrs.js +14 -0
  106. package/utils/wrapPromiseCallback.js +36 -0
  107. package/versioned_data_kind.js +34 -0
package/changes.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "bumped": {
3
+ "async": {
4
+ "from": "^3.2.4",
5
+ "to": "^3.2.6"
6
+ },
7
+ "launchdarkly-eventsource": {
8
+ "from": "1.4.4",
9
+ "to": "^2.2.0"
10
+ },
11
+ "lru-cache": {
12
+ "from": "^6.0.0",
13
+ "to": "^11.2.7"
14
+ },
15
+ "node-cache": {
16
+ "from": "^5.1.0",
17
+ "to": "^5.1.2"
18
+ },
19
+ "semver": {
20
+ "from": "^7.5.4",
21
+ "to": "^7.7.4"
22
+ },
23
+ "uuid": {
24
+ "from": "^8.3.2",
25
+ "to": "^13.0.0"
26
+ }
27
+ },
28
+ "timestamp": "2026-03-17T22:56:41.641Z",
29
+ "totalUpdated": 6
30
+ }
@@ -0,0 +1,235 @@
1
+ const InMemoryFeatureStore = require('./feature_store');
2
+ const loggers = require('./loggers');
3
+ const messages = require('./messages');
4
+
5
+ module.exports = (function () {
6
+ const defaults = function () {
7
+ return {
8
+ baseUri: 'https://app.launchdarkly.com',
9
+ streamUri: 'https://stream.launchdarkly.com',
10
+ eventsUri: 'https://events.launchdarkly.com',
11
+ stream: true,
12
+ streamInitialReconnectDelay: 1,
13
+ sendEvents: true,
14
+ timeout: 5,
15
+ capacity: 10000,
16
+ flushInterval: 5,
17
+ pollInterval: 30,
18
+ offline: false,
19
+ useLdd: false,
20
+ allAttributesPrivate: false,
21
+ privateAttributes: [],
22
+ contextKeysCapacity: 1000,
23
+ contextKeysFlushInterval: 300,
24
+ diagnosticOptOut: false,
25
+ diagnosticRecordingInterval: 900,
26
+ featureStore: InMemoryFeatureStore(),
27
+ };
28
+ };
29
+
30
+ const typesForPropertiesWithNoDefault = {
31
+ // Add a value here if we add a configuration property whose type cannot be determined by looking
32
+ // in baseDefaults (for instance, the default is null but if the value isn't null it should be a
33
+ // string). The allowable values are 'boolean', 'string', 'number', 'object', 'function', 'array' or
34
+ // 'factory' (the last one means it can be either a function or an object).
35
+ bigSegments: 'object',
36
+ eventProcessor: 'object',
37
+ featureStore: 'object',
38
+ logger: 'object', // LDLogger
39
+ proxyAgent: 'object',
40
+ proxyAuth: 'string',
41
+ proxyHost: 'string',
42
+ proxyPort: 'number',
43
+ proxyScheme: 'string',
44
+ tlsParams: 'object', // LDTLSOptions
45
+ updateProcessor: 'factory', // gets special handling in validation
46
+ wrapperName: 'string',
47
+ wrapperVersion: 'string',
48
+ };
49
+
50
+ /**
51
+ * Expression to validate characters that are allowed in tag keys and values.
52
+ */
53
+ const allowedTagCharacters = /^(\w|\.|-)+$/;
54
+
55
+ /**
56
+ * Verify that a value meets the requirements for a tag value.
57
+ * @param {Object} config
58
+ * @param {string} tagValue
59
+ */
60
+ function validateTagValue(name, config, tagValue) {
61
+ if (typeof tagValue !== 'string' || !tagValue.match(allowedTagCharacters)) {
62
+ config.logger.warn(messages.invalidTagValue(name));
63
+ return undefined;
64
+ }
65
+ if (tagValue.length > 64) {
66
+ config.logger.warn(messages.tagValueTooLong(name));
67
+ return undefined;
68
+ }
69
+ return tagValue;
70
+ }
71
+
72
+ const optionsWithValidatorsOrConversions = {
73
+ // Add a value here if we add a configuration property which requires custom validation
74
+ // and/or type conversion.
75
+ // The validator should log a message for any validation issues encountered.
76
+ // The validator should return undefined, or the validated value.
77
+
78
+ application: (name, config, value) => {
79
+ const validated = {};
80
+ if (value.id) {
81
+ validated.id = validateTagValue(`${name}.id`, config, value.id);
82
+ }
83
+ if (value.version) {
84
+ validated.version = validateTagValue(`${name}.version`, config, value.version);
85
+ }
86
+ return validated;
87
+ },
88
+ };
89
+
90
+ /* eslint-disable camelcase */
91
+ const deprecatedOptions = {
92
+ userKeysCapacity: 'contextKeysCapacity',
93
+ userKeysFlushInterval: 'contextKeysFlushInterval',
94
+ };
95
+ /* eslint-enable camelcase */
96
+
97
+ function checkDeprecatedOptions(configIn) {
98
+ const config = configIn;
99
+ Object.keys(deprecatedOptions).forEach(oldName => {
100
+ if (config[oldName] !== undefined) {
101
+ const newName = deprecatedOptions[oldName];
102
+ config.logger.warn(messages.deprecated(oldName, newName));
103
+ if (config[newName] === undefined) {
104
+ config[newName] = config[oldName];
105
+ }
106
+ delete config[oldName];
107
+ }
108
+ });
109
+ }
110
+
111
+ function applyDefaults(config, defaults) {
112
+ // This works differently from Object.assign() in that it will *not* override a default value
113
+ // if the provided value is explicitly set to null.
114
+ const ret = Object.assign({}, config);
115
+ Object.keys(defaults).forEach(name => {
116
+ if (ret[name] === undefined || ret[name] === null) {
117
+ ret[name] = defaults[name];
118
+ }
119
+ });
120
+ return ret;
121
+ }
122
+
123
+ function canonicalizeUri(uri) {
124
+ return uri.replace(/\/+$/, '');
125
+ }
126
+
127
+ function validateTypesAndNames(configIn, defaultConfig) {
128
+ const config = configIn;
129
+ const typeDescForValue = value => {
130
+ if (value === null || value === undefined) {
131
+ return undefined;
132
+ }
133
+ if (Array.isArray(value)) {
134
+ return 'array';
135
+ }
136
+ const t = typeof value;
137
+ if (t === 'boolean' || t === 'string' || t === 'number') {
138
+ return t;
139
+ }
140
+ return 'object';
141
+ };
142
+
143
+ Object.keys(config).forEach(name => {
144
+ const value = config[name];
145
+ if (value !== null && value !== undefined) {
146
+ const defaultValue = defaultConfig[name];
147
+ const typeDesc = typesForPropertiesWithNoDefault[name];
148
+ const validator = optionsWithValidatorsOrConversions[name];
149
+ if (defaultValue === undefined && typeDesc === undefined && validator === undefined) {
150
+ config.logger.warn(messages.unknownOption(name));
151
+ } else if (validator !== undefined) {
152
+ const validated = validator(name, config, config[name]);
153
+ if (validated !== undefined) {
154
+ config[name] = validated;
155
+ } else {
156
+ delete config[name];
157
+ }
158
+ } else {
159
+ const expectedType = typeDesc || typeDescForValue(defaultValue);
160
+ const actualType = typeDescForValue(value);
161
+ if (actualType !== expectedType) {
162
+ if (expectedType === 'factory' && (typeof value === 'function' || typeof value === 'object')) {
163
+ // for some properties, we allow either a factory function or an instance
164
+ return;
165
+ }
166
+ if (expectedType === 'boolean') {
167
+ config[name] = !!value;
168
+ config.logger.warn(messages.wrongOptionTypeBoolean(name, actualType));
169
+ } else {
170
+ config.logger.warn(messages.wrongOptionType(name, expectedType, actualType));
171
+ config[name] = defaultConfig[name];
172
+ }
173
+ }
174
+ }
175
+ }
176
+ });
177
+ }
178
+
179
+ function enforceMinimum(configIn, name, min) {
180
+ const config = configIn;
181
+ if (config[name] < min) {
182
+ config.logger.warn(messages.optionBelowMinimum(name, config[name], min));
183
+ config[name] = min;
184
+ }
185
+ }
186
+
187
+ function validate(options) {
188
+ let config = Object.assign({}, options || {});
189
+
190
+ const fallbackLogger = loggers.basicLogger({ level: 'info' });
191
+ config.logger = config.logger ? loggers.safeLogger(config.logger, fallbackLogger) : fallbackLogger;
192
+
193
+ checkDeprecatedOptions(config);
194
+
195
+ const defaultConfig = defaults();
196
+ config = applyDefaults(config, defaultConfig);
197
+
198
+ validateTypesAndNames(config, defaultConfig);
199
+
200
+ config.baseUri = canonicalizeUri(config.baseUri);
201
+ config.streamUri = canonicalizeUri(config.streamUri);
202
+ config.eventsUri = canonicalizeUri(config.eventsUri);
203
+
204
+ enforceMinimum(config, 'pollInterval', 30);
205
+ enforceMinimum(config, 'diagnosticRecordingInterval', 60);
206
+
207
+ return config;
208
+ }
209
+
210
+ /**
211
+ * Get tags for the specified configuration.
212
+ *
213
+ * If any additional tags are added to the configuration, then the tags from
214
+ * this method should be extended with those.
215
+ * @param {Object} config The already valiated configuration.
216
+ * @returns {Object} The tag configuration.
217
+ */
218
+ function getTags(config) {
219
+ const tags = {};
220
+ if (config.application && config.application.id !== undefined && config.application.id !== null) {
221
+ tags['application-id'] = [config.application.id];
222
+ }
223
+ if (config.application && config.application.version !== undefined && config.application.id !== null) {
224
+ tags['application-version'] = [config.application.version];
225
+ }
226
+
227
+ return tags;
228
+ }
229
+
230
+ return {
231
+ validate,
232
+ defaults,
233
+ getTags,
234
+ };
235
+ })();
package/context.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Validate a context kind.
3
+ * @param {string} kind
4
+ * @returns true if the kind is valid.
5
+ */
6
+ function validKind(kind) {
7
+ return typeof kind === 'string' && kind !== 'kind' && kind.match(/^(\w|\.|-)+$/);
8
+ }
9
+
10
+ /**
11
+ * Validate a context key.
12
+ * @param {string} key
13
+ * @returns true if the key is valid.
14
+ */
15
+ function validKey(key) {
16
+ return key !== undefined && key !== null && key !== '' && typeof key === 'string';
17
+ }
18
+
19
+ /**
20
+ * Perform a check of basic context requirements.
21
+ * @param {Object} context
22
+ * @param {boolean} allowLegacyKey If true, then a legacy user can have an
23
+ * empty or non-string key. A legacy user is a context without a kind.
24
+ * @returns true if the context meets basic requirements.
25
+ */
26
+ function checkContext(context, allowLegacyKey) {
27
+ if (context) {
28
+ if (allowLegacyKey && (context.kind === undefined || context.kind === null)) {
29
+ return context.key !== undefined && context.key !== null;
30
+ }
31
+ const key = context.key;
32
+ const kind = context.kind === undefined ? 'user' : context.kind;
33
+ const kindValid = validKind(kind);
34
+ const keyValid = kind === 'multi' || validKey(key);
35
+ if (kind === 'multi') {
36
+ const kinds = Object.keys(context).filter(key => key !== 'kind');
37
+ return keyValid && kinds.every(key => validKind(key)) && kinds.every(key => validKey(context[key].key));
38
+ }
39
+ return keyValid && kindValid;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ /**
45
+ * The partial URL encoding is needed because : is a valid character in context keys.
46
+ *
47
+ * Partial encoding is the replacement of all colon (:) characters with the URL
48
+ * encoded equivalent (%3A) and all percent (%) characters with the URL encoded
49
+ * equivalent (%25).
50
+ * @param {string} key The key to encode.
51
+ * @returns {string} Partially URL encoded key.
52
+ */
53
+ function encodeKey(key) {
54
+ if (key.includes('%') || key.includes(':')) {
55
+ return key.replace(/%/g, '%25').replace(/:/g, '%3A');
56
+ }
57
+ return key;
58
+ }
59
+
60
+ /**
61
+ * For a given context get a list of context kinds.
62
+ * @param {Object} context
63
+ * @returns A list of kinds in the context.
64
+ */
65
+ function getContextKinds(context) {
66
+ if (context) {
67
+ if (context.kind === null || context.kind === undefined) {
68
+ return ['user'];
69
+ }
70
+ if (context.kind !== 'multi') {
71
+ return [context.kind];
72
+ }
73
+ return Object.keys(context).filter(kind => kind !== 'kind');
74
+ }
75
+ return [];
76
+ }
77
+
78
+ function getCanonicalKey(context) {
79
+ if (context) {
80
+ if ((context.kind === undefined || context.kind === null || context.kind === 'user') && context.key) {
81
+ return context.key;
82
+ } else if (context.kind !== 'multi' && context.key) {
83
+ return `${context.kind}:${encodeKey(context.key)}`;
84
+ } else if (context.kind === 'multi') {
85
+ return Object.keys(context)
86
+ .sort()
87
+ .filter(key => key !== 'kind')
88
+ .map(key => `${key}:${encodeKey(context[key].key)}`)
89
+ .join(':');
90
+ }
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ checkContext,
96
+ getContextKinds,
97
+ getCanonicalKey,
98
+ };
@@ -0,0 +1,137 @@
1
+ const AttributeReference = require('./attribute_reference');
2
+
3
+ function ContextFilter(config) {
4
+ const filter = {};
5
+
6
+ const allAttributesPrivate = config.allAttributesPrivate;
7
+ const privateAttributes = config.privateAttributes || [];
8
+
9
+ // These attributes cannot be removed via a private attribute.
10
+ const protectedAttributes = ['key', 'kind', '_meta', 'anonymous'];
11
+
12
+ const legacyTopLevelCopyAttributes = ['name', 'ip', 'firstName', 'lastName', 'email', 'avatar', 'country'];
13
+
14
+ /**
15
+ * For the given context and configuration get a list of attributes to filter.
16
+ * @param {Object} context
17
+ * @returns {string[]} A list of the attributes to filter.
18
+ */
19
+ const getAttributesToFilter = context =>
20
+ (allAttributesPrivate
21
+ ? Object.keys(context)
22
+ : [...privateAttributes, ...((context._meta && context._meta.privateAttributes) || [])]
23
+ ).filter(attr => !protectedAttributes.some(protectedAttr => AttributeReference.compare(attr, protectedAttr)));
24
+
25
+ /**
26
+ * @param {Object} context
27
+ * @returns {Object} A copy of the context with private attributes removed,
28
+ * and the redactedAttributes meta populated.
29
+ */
30
+ const filterSingleKind = context => {
31
+ if (typeof context !== 'object' || context === null || Array.isArray(context)) {
32
+ return undefined;
33
+ }
34
+
35
+ const { cloned, excluded } = AttributeReference.cloneExcluding(context, getAttributesToFilter(context));
36
+ cloned.key = String(cloned.key);
37
+ if (excluded.length) {
38
+ if (!cloned._meta) {
39
+ cloned._meta = {};
40
+ }
41
+ cloned._meta.redactedAttributes = excluded;
42
+ }
43
+ if (cloned._meta) {
44
+ delete cloned._meta['privateAttributes'];
45
+ if (Object.keys(cloned._meta).length === 0) {
46
+ delete cloned._meta;
47
+ }
48
+ }
49
+ // Make sure anonymous is boolean if present.
50
+ // Null counts as present, and would be falsy, which is the default.
51
+ if (cloned.anonymous !== undefined) {
52
+ cloned.anonymous = !!cloned.anonymous;
53
+ }
54
+
55
+ return cloned;
56
+ };
57
+
58
+ /**
59
+ * @param {Object} context
60
+ * @returns {Object} A copy of the context with the private attributes removed,
61
+ * and the redactedAttributes meta populated for each sub-context.
62
+ */
63
+ const filterMultiKind = context => {
64
+ const filtered = {
65
+ kind: context.kind,
66
+ };
67
+ const contextKeys = Object.keys(context);
68
+
69
+ for (const contextKey of contextKeys) {
70
+ if (contextKey !== 'kind') {
71
+ const filteredContext = filterSingleKind(context[contextKey]);
72
+ if (filteredContext) {
73
+ filtered[contextKey] = filteredContext;
74
+ }
75
+ }
76
+ }
77
+ return filtered;
78
+ };
79
+
80
+ /**
81
+ * Convert the LDUser object into an LDContext object.
82
+ * @param {Object} user The LDUser to produce an LDContext for.
83
+ * @returns {Object} A single kind context based on the provided user.
84
+ */
85
+ const legacyToSingleKind = user => {
86
+ const filtered = {
87
+ /* Destructure custom items into the top level.
88
+ Duplicate keys will be overridden by previously
89
+ top level items.
90
+ */
91
+ ...(user.custom || {}),
92
+
93
+ // Implicity a user kind.
94
+ kind: 'user',
95
+
96
+ key: user.key,
97
+ };
98
+
99
+ if (user.anonymous !== undefined) {
100
+ filtered.anonymous = !!user.anonymous;
101
+ }
102
+
103
+ // Copy top level keys and convert them to strings.
104
+ // Remove keys that may have been destructured from `custom`.
105
+ for (const key of legacyTopLevelCopyAttributes) {
106
+ delete filtered[key];
107
+ if (user[key] !== undefined && user[key] !== null) {
108
+ filtered[key] = String(user[key]);
109
+ }
110
+ }
111
+
112
+ if (user.privateAttributeNames !== undefined && user.privateAttributeNames !== null) {
113
+ filtered._meta = filtered._meta || {};
114
+ // If any private attributes started with '/' we need to convert them to references, otherwise the '/' will
115
+ // cause the literal to incorrectly be treated as a reference.
116
+ filtered._meta.privateAttributes = user.privateAttributeNames.map(literal =>
117
+ literal.startsWith('/') ? AttributeReference.literalToReference(literal) : literal
118
+ );
119
+ }
120
+
121
+ return filtered;
122
+ };
123
+
124
+ filter.filter = context => {
125
+ if (context.kind === undefined || context.kind === null) {
126
+ return filterSingleKind(legacyToSingleKind(context));
127
+ } else if (context.kind === 'multi') {
128
+ return filterMultiKind(context);
129
+ } else {
130
+ return filterSingleKind(context);
131
+ }
132
+ };
133
+
134
+ return filter;
135
+ }
136
+
137
+ module.exports = ContextFilter;
@@ -0,0 +1,7 @@
1
+ # SDK contract test service
2
+
3
+ This directory contains an implementation of the cross-platform SDK testing protocol defined by https://github.com/launchdarkly/sdk-test-harness. See that project's `README` for details of this protocol, and the kinds of SDK capabilities that are relevant to the contract tests. This code should not need to be updated unless the SDK has added or removed such capabilities.
4
+
5
+ To run these tests locally, run `npm run contract-tests` from the SDK project root directory. This will start the test service, download the correct version of the test harness tool, and run the tests.
6
+
7
+ Or, to test against an in-progress local version of the test harness, run `npm run contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line.
@@ -0,0 +1,109 @@
1
+ const express = require('express');
2
+ const bodyParser = require('body-parser');
3
+
4
+ const { Log } = require('./log');
5
+ const { newSdkClientEntity, badCommandError } = require('./sdkClientEntity');
6
+
7
+ const app = express();
8
+ let server = null;
9
+
10
+ const port = 8000;
11
+
12
+ let clientCounter = 0;
13
+ const clients = {};
14
+
15
+ const mainLog = Log('service');
16
+
17
+ app.use(bodyParser.json());
18
+
19
+ app.get('/', (req, res) => {
20
+ res.header('Content-Type', 'application/json');
21
+ res.json({
22
+ capabilities: [
23
+ 'server-side',
24
+ 'all-flags-client-side-only',
25
+ 'all-flags-details-only-for-tracked-flags',
26
+ 'all-flags-with-reasons',
27
+ 'tags',
28
+ 'user-type',
29
+ ],
30
+ });
31
+ });
32
+
33
+ app.delete('/', (req, res) => {
34
+ mainLog.info('Test service has told us to exit');
35
+ res.status(204);
36
+ res.send();
37
+
38
+ // Defer the following actions till after the response has been sent
39
+ setTimeout(() => {
40
+ server.close(() => process.exit());
41
+ // We force-quit with process.exit because, even after closing the server, there could be some
42
+ // scheduled tasks lingering if an SDK instance didn't get cleaned up properly, and we don't want
43
+ // that to prevent us from quitting.
44
+ }, 1);
45
+ });
46
+
47
+ app.post('/', async (req, res) => {
48
+ const options = req.body;
49
+
50
+ clientCounter += 1;
51
+ const clientId = clientCounter.toString();
52
+ const resourceUrl = `/clients/${clientId}`;
53
+
54
+ try {
55
+ const client = await newSdkClientEntity(options);
56
+ clients[clientId] = client;
57
+
58
+ res.status(201);
59
+ res.set('Location', resourceUrl);
60
+ } catch (e) {
61
+ res.status(500);
62
+ const message = e.message || JSON.stringify(e);
63
+ mainLog.error('Error creating client: ' + message);
64
+ res.write(message);
65
+ }
66
+ res.send();
67
+ });
68
+
69
+ app.post('/clients/:id', async (req, res) => {
70
+ const client = clients[req.params.id];
71
+ if (!client) {
72
+ res.status(404);
73
+ } else {
74
+ try {
75
+ const respValue = await client.doCommand(req.body);
76
+ if (respValue) {
77
+ res.status(200);
78
+ res.write(JSON.stringify(respValue));
79
+ } else {
80
+ res.status(204);
81
+ }
82
+ } catch (e) {
83
+ const isBadRequest = e === badCommandError;
84
+ res.status(isBadRequest ? 400 : 500);
85
+ res.write(e.message || JSON.stringify(e));
86
+ if (!isBadRequest && e.stack) {
87
+ console.log(e.stack);
88
+ }
89
+ }
90
+ }
91
+ res.send();
92
+ });
93
+
94
+ app.delete('/clients/:id', async (req, res) => {
95
+ const client = clients[req.params.id];
96
+ if (!client) {
97
+ res.status(404);
98
+ res.send();
99
+ } else {
100
+ client.close();
101
+ delete clients[req.params.id];
102
+ res.status(204);
103
+ res.send();
104
+ }
105
+ });
106
+
107
+ server = app.listen(port, () => {
108
+ console.log('Listening on port %d', port);
109
+ });
@@ -0,0 +1,23 @@
1
+ const ld = require('launchdarkly-node-server-sdk');
2
+
3
+ function Log(tag) {
4
+ function doLog(level, message) {
5
+ console.log(new Date().toISOString() + ` [${tag}] ${level}: ${message}`);
6
+ }
7
+ return {
8
+ info: message => doLog('info', message),
9
+ error: message => doLog('error', message),
10
+ };
11
+ }
12
+
13
+ function sdkLogger(tag) {
14
+ return ld.basicLogger({
15
+ level: 'debug',
16
+ destination: line => {
17
+ console.log(new Date().toISOString() + ` [${tag}.sdk] ${line}`);
18
+ },
19
+ });
20
+ }
21
+
22
+ module.exports.Log = Log;
23
+ module.exports.sdkLogger = sdkLogger;
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "node-server-sdk-contract-tests",
3
+ "version": "0.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "start": "node index.js"
7
+ },
8
+ "author": "",
9
+ "license": "Apache-2.0",
10
+ "dependencies": {
11
+ "body-parser": "^1.19.0",
12
+ "express": "^4.17.1",
13
+ "launchdarkly-node-server-sdk": "file:.."
14
+ }
15
+ }