@ember-data/request-utils 5.5.0-alpha.2 → 5.5.0-alpha.21

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 (41) hide show
  1. package/LICENSE.md +19 -7
  2. package/README.md +12 -2
  3. package/addon-main.cjs +5 -0
  4. package/dist/deprecation-support.js +187 -0
  5. package/dist/deprecation-support.js.map +1 -0
  6. package/dist/index.js +776 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/inflect-8aYUyMN7.js +311 -0
  9. package/dist/inflect-8aYUyMN7.js.map +1 -0
  10. package/dist/string.js +1 -0
  11. package/dist/string.js.map +1 -0
  12. package/logos/NCC-1701-a-blue.svg +4 -0
  13. package/logos/NCC-1701-a-gold.svg +4 -0
  14. package/logos/NCC-1701-a-gold_100.svg +1 -0
  15. package/logos/NCC-1701-a-gold_base-64.txt +1 -0
  16. package/logos/NCC-1701-a.svg +4 -0
  17. package/logos/README.md +4 -0
  18. package/logos/docs-badge.svg +2 -0
  19. package/logos/github-header.svg +444 -0
  20. package/logos/social1.png +0 -0
  21. package/logos/social2.png +0 -0
  22. package/logos/warp-drive-logo-dark.svg +4 -0
  23. package/logos/warp-drive-logo-gold.svg +4 -0
  24. package/package.json +59 -29
  25. package/unstable-preview-types/-private/string/inflect.d.ts +14 -0
  26. package/unstable-preview-types/-private/string/inflect.d.ts.map +1 -0
  27. package/unstable-preview-types/-private/string/inflections.d.ts +12 -0
  28. package/unstable-preview-types/-private/string/inflections.d.ts.map +1 -0
  29. package/unstable-preview-types/-private/string/transform.d.ts +81 -0
  30. package/unstable-preview-types/-private/string/transform.d.ts.map +1 -0
  31. package/unstable-preview-types/deprecation-support.d.ts +4 -0
  32. package/unstable-preview-types/deprecation-support.d.ts.map +1 -0
  33. package/unstable-preview-types/index.d.ts +514 -0
  34. package/unstable-preview-types/index.d.ts.map +1 -0
  35. package/unstable-preview-types/string.d.ts +5 -0
  36. package/unstable-preview-types/string.d.ts.map +1 -0
  37. package/addon/index.js +0 -424
  38. package/addon/index.js.map +0 -1
  39. package/addon-main.js +0 -19
  40. /package/{ember-data-logo-dark.svg → logos/ember-data-logo-dark.svg} +0 -0
  41. /package/{ember-data-logo-light.svg → logos/ember-data-logo-light.svg} +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,776 @@
1
+ import { deprecate } from '@ember/debug';
2
+ import { getOrSetGlobal } from '@warp-drive/core-types/-private';
3
+ import { macroCondition, getGlobalConfig } from '@embroider/macros';
4
+
5
+ /**
6
+ * Simple utility function to assist in url building,
7
+ * query params, and other common request operations.
8
+ *
9
+ * These primitives may be used directly or composed
10
+ * by request builders to provide a consistent interface
11
+ * for building requests.
12
+ *
13
+ * For instance:
14
+ *
15
+ * ```ts
16
+ * import { buildBaseURL, buildQueryParams } from '@ember-data/request-utils';
17
+ *
18
+ * const baseURL = buildBaseURL({
19
+ * host: 'https://api.example.com',
20
+ * namespace: 'api/v1',
21
+ * resourcePath: 'emberDevelopers',
22
+ * op: 'query',
23
+ * identifier: { type: 'ember-developer' }
24
+ * });
25
+ * const url = `${baseURL}?${buildQueryParams({ name: 'Chris', include:['pets'] })}`;
26
+ * // => 'https://api.example.com/api/v1/emberDevelopers?include=pets&name=Chris'
27
+ * ```
28
+ *
29
+ * This is useful, but not as useful as the REST request builder for query which is sugar
30
+ * over this (and more!):
31
+ *
32
+ * ```ts
33
+ * import { query } from '@ember-data/rest/request';
34
+ *
35
+ * const options = query('ember-developer', { name: 'Chris', include:['pets'] });
36
+ * // => { url: 'https://api.example.com/api/v1/emberDevelopers?include=pets&name=Chris' }
37
+ * // Note: options will also include other request options like headers, method, etc.
38
+ * ```
39
+ *
40
+ * @module @ember-data/request-utils
41
+ * @main @ember-data/request-utils
42
+ * @public
43
+ */
44
+ // prevents the final constructed object from needing to add
45
+ // host and namespace which are provided by the final consuming
46
+ // class to the prototype which can result in overwrite errors
47
+ const CONFIG = getOrSetGlobal('CONFIG', {
48
+ host: '',
49
+ namespace: ''
50
+ });
51
+
52
+ /**
53
+ * Sets the global configuration for `buildBaseURL`
54
+ * for host and namespace values for the application.
55
+ *
56
+ * These values may still be overridden by passing
57
+ * them to buildBaseURL directly.
58
+ *
59
+ * This method may be called as many times as needed.
60
+ * host values of `''` or `'/'` are equivalent.
61
+ *
62
+ * Except for the value of `/` as host, host should not
63
+ * end with `/`.
64
+ *
65
+ * namespace should not start or end with a `/`.
66
+ *
67
+ * ```ts
68
+ * type BuildURLConfig = {
69
+ * host: string;
70
+ * namespace: string'
71
+ * }
72
+ * ```
73
+ *
74
+ * Example:
75
+ *
76
+ * ```ts
77
+ * import { setBuildURLConfig } from '@ember-data/request-utils';
78
+ *
79
+ * setBuildURLConfig({
80
+ * host: 'https://api.example.com',
81
+ * namespace: 'api/v1'
82
+ * });
83
+ * ```
84
+ *
85
+ * @method setBuildURLConfig
86
+ * @static
87
+ * @public
88
+ * @for @ember-data/request-utils
89
+ * @param {BuildURLConfig} config
90
+ * @return void
91
+ */
92
+ function setBuildURLConfig(config) {
93
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
94
+ if (!test) {
95
+ throw new Error(`setBuildURLConfig: You must pass a config object`);
96
+ }
97
+ })(config) : {};
98
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
99
+ if (!test) {
100
+ throw new Error(`setBuildURLConfig: You must pass a config object with a 'host' or 'namespace' property`);
101
+ }
102
+ })('host' in config || 'namespace' in config) : {};
103
+ CONFIG.host = config.host || '';
104
+ CONFIG.namespace = config.namespace || '';
105
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
106
+ if (!test) {
107
+ throw new Error(`buildBaseURL: host must NOT end with '/', received '${CONFIG.host}'`);
108
+ }
109
+ })(CONFIG.host === '/' || !CONFIG.host.endsWith('/')) : {};
110
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
111
+ if (!test) {
112
+ throw new Error(`buildBaseURL: namespace must NOT start with '/', received '${CONFIG.namespace}'`);
113
+ }
114
+ })(!CONFIG.namespace.startsWith('/')) : {};
115
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
116
+ if (!test) {
117
+ throw new Error(`buildBaseURL: namespace must NOT end with '/', received '${CONFIG.namespace}'`);
118
+ }
119
+ })(!CONFIG.namespace.endsWith('/')) : {};
120
+ }
121
+ const OPERATIONS_WITH_PRIMARY_RECORDS = new Set(['findRecord', 'findRelatedRecord', 'findRelatedCollection', 'updateRecord', 'deleteRecord']);
122
+ function isOperationWithPrimaryRecord(options) {
123
+ return 'op' in options && OPERATIONS_WITH_PRIMARY_RECORDS.has(options.op);
124
+ }
125
+ function hasResourcePath(options) {
126
+ return 'resourcePath' in options && typeof options.resourcePath === 'string' && options.resourcePath.length > 0;
127
+ }
128
+ function resourcePathForType(options) {
129
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
130
+ if (!test) {
131
+ throw new Error(`resourcePathForType: You must pass a valid op as part of options`);
132
+ }
133
+ })('op' in options && typeof options.op === 'string') : {};
134
+ return options.op === 'findMany' ? options.identifiers[0].type : options.identifier.type;
135
+ }
136
+
137
+ /**
138
+ * Builds a URL for a request based on the provided options.
139
+ * Does not include support for building query params (see `buildQueryParams`)
140
+ * so that it may be composed cleanly with other query-params strategies.
141
+ *
142
+ * Usage:
143
+ *
144
+ * ```ts
145
+ * import { buildBaseURL } from '@ember-data/request-utils';
146
+ *
147
+ * const url = buildBaseURL({
148
+ * host: 'https://api.example.com',
149
+ * namespace: 'api/v1',
150
+ * resourcePath: 'emberDevelopers',
151
+ * op: 'query',
152
+ * identifier: { type: 'ember-developer' }
153
+ * });
154
+ *
155
+ * // => 'https://api.example.com/api/v1/emberDevelopers'
156
+ * ```
157
+ *
158
+ * On the surface this may seem like a lot of work to do something simple, but
159
+ * it is designed to be composable with other utilities and interfaces that the
160
+ * average product engineer will never need to see or use.
161
+ *
162
+ * A few notes:
163
+ *
164
+ * - `resourcePath` is optional, but if it is not provided, `identifier.type` will be used.
165
+ * - `host` and `namespace` are optional, but if they are not provided, the values globally
166
+ * configured via `setBuildURLConfig` will be used.
167
+ * - `op` is required and must be one of the following:
168
+ * - 'findRecord' 'query' 'findMany' 'findRelatedCollection' 'findRelatedRecord'` 'createRecord' 'updateRecord' 'deleteRecord'
169
+ * - Depending on the value of `op`, `identifier` or `identifiers` will be required.
170
+ *
171
+ * @method buildBaseURL
172
+ * @static
173
+ * @public
174
+ * @for @ember-data/request-utils
175
+ * @param urlOptions
176
+ * @return string
177
+ */
178
+ function buildBaseURL(urlOptions) {
179
+ const options = Object.assign({
180
+ host: CONFIG.host,
181
+ namespace: CONFIG.namespace
182
+ }, urlOptions);
183
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
184
+ if (!test) {
185
+ throw new Error(`buildBaseURL: You must pass \`op\` as part of options`);
186
+ }
187
+ })(hasResourcePath(options) || typeof options.op === 'string' && options.op.length > 0) : {};
188
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
189
+ if (!test) {
190
+ throw new Error(`buildBaseURL: You must pass \`identifier\` as part of options`);
191
+ }
192
+ })(hasResourcePath(options) || options.op === 'findMany' || options.identifier && typeof options.identifier === 'object') : {};
193
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
194
+ if (!test) {
195
+ throw new Error(`buildBaseURL: You must pass \`identifiers\` as part of options`);
196
+ }
197
+ })(hasResourcePath(options) || options.op !== 'findMany' || options.identifiers && Array.isArray(options.identifiers) && options.identifiers.length > 0 && options.identifiers.every(i => i && typeof i === 'object')) : {};
198
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
199
+ if (!test) {
200
+ throw new Error(`buildBaseURL: You must pass valid \`identifier\` as part of options, expected 'id'`);
201
+ }
202
+ })(hasResourcePath(options) || !isOperationWithPrimaryRecord(options) || typeof options.identifier.id === 'string' && options.identifier.id.length > 0) : {};
203
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
204
+ if (!test) {
205
+ throw new Error(`buildBaseURL: You must pass \`identifiers\` as part of options`);
206
+ }
207
+ })(hasResourcePath(options) || options.op !== 'findMany' || options.identifiers.every(i => typeof i.id === 'string' && i.id.length > 0)) : {};
208
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
209
+ if (!test) {
210
+ throw new Error(`buildBaseURL: You must pass valid \`identifier\` as part of options, expected 'type'`);
211
+ }
212
+ })(hasResourcePath(options) || options.op === 'findMany' || typeof options.identifier.type === 'string' && options.identifier.type.length > 0) : {};
213
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
214
+ if (!test) {
215
+ throw new Error(`buildBaseURL: You must pass valid \`identifiers\` as part of options, expected 'type'`);
216
+ }
217
+ })(hasResourcePath(options) || options.op !== 'findMany' || typeof options.identifiers[0].type === 'string' && options.identifiers[0].type.length > 0) : {};
218
+
219
+ // prettier-ignore
220
+ const idPath = isOperationWithPrimaryRecord(options) ? encodeURIComponent(options.identifier.id) : '';
221
+ const resourcePath = options.resourcePath || resourcePathForType(options);
222
+ const {
223
+ host,
224
+ namespace
225
+ } = options;
226
+ const fieldPath = 'fieldPath' in options ? options.fieldPath : '';
227
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
228
+ if (!test) {
229
+ throw new Error(`buildBaseURL: You tried to build a url for a ${String('op' in options ? options.op + ' ' : '')}request to ${resourcePath} but resourcePath must be set or op must be one of "${['findRecord', 'findRelatedRecord', 'findRelatedCollection', 'updateRecord', 'deleteRecord', 'createRecord', 'query', 'findMany'].join('","')}".`);
230
+ }
231
+ })(hasResourcePath(options) || ['findRecord', 'query', 'findMany', 'findRelatedCollection', 'findRelatedRecord', 'createRecord', 'updateRecord', 'deleteRecord'].includes(options.op)) : {};
232
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
233
+ if (!test) {
234
+ throw new Error(`buildBaseURL: host must NOT end with '/', received '${host}'`);
235
+ }
236
+ })(host === '/' || !host.endsWith('/')) : {};
237
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
238
+ if (!test) {
239
+ throw new Error(`buildBaseURL: namespace must NOT start with '/', received '${namespace}'`);
240
+ }
241
+ })(!namespace.startsWith('/')) : {};
242
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
243
+ if (!test) {
244
+ throw new Error(`buildBaseURL: namespace must NOT end with '/', received '${namespace}'`);
245
+ }
246
+ })(!namespace.endsWith('/')) : {};
247
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
248
+ if (!test) {
249
+ throw new Error(`buildBaseURL: resourcePath must NOT start with '/', received '${resourcePath}'`);
250
+ }
251
+ })(!resourcePath.startsWith('/')) : {};
252
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
253
+ if (!test) {
254
+ throw new Error(`buildBaseURL: resourcePath must NOT end with '/', received '${resourcePath}'`);
255
+ }
256
+ })(!resourcePath.endsWith('/')) : {};
257
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
258
+ if (!test) {
259
+ throw new Error(`buildBaseURL: fieldPath must NOT start with '/', received '${fieldPath}'`);
260
+ }
261
+ })(!fieldPath.startsWith('/')) : {};
262
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
263
+ if (!test) {
264
+ throw new Error(`buildBaseURL: fieldPath must NOT end with '/', received '${fieldPath}'`);
265
+ }
266
+ })(!fieldPath.endsWith('/')) : {};
267
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
268
+ if (!test) {
269
+ throw new Error(`buildBaseURL: idPath must NOT start with '/', received '${idPath}'`);
270
+ }
271
+ })(!idPath.startsWith('/')) : {};
272
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
273
+ if (!test) {
274
+ throw new Error(`buildBaseURL: idPath must NOT end with '/', received '${idPath}'`);
275
+ }
276
+ })(!idPath.endsWith('/')) : {};
277
+ const hasHost = host !== '' && host !== '/';
278
+ const url = [hasHost ? host : '', namespace, resourcePath, idPath, fieldPath].filter(Boolean).join('/');
279
+ return hasHost ? url : `/${url}`;
280
+ }
281
+ const DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS = {
282
+ arrayFormat: 'comma'
283
+ };
284
+ function handleInclude(include) {
285
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
286
+ if (!test) {
287
+ throw new Error(`Expected include to be a string or array, got ${typeof include}`);
288
+ }
289
+ })(typeof include === 'string' || Array.isArray(include)) : {};
290
+ return typeof include === 'string' ? include.split(',') : include;
291
+ }
292
+
293
+ /**
294
+ * filter out keys of an object that have falsy values or point to empty arrays
295
+ * returning a new object with only those keys that have truthy values / non-empty arrays
296
+ *
297
+ * @method filterEmpty
298
+ * @static
299
+ * @public
300
+ * @for @ember-data/request-utils
301
+ * @param {Record<string, Serializable>} source object to filter keys with empty values from
302
+ * @return {Record<string, Serializable>} A new object with the keys that contained empty values removed
303
+ */
304
+ function filterEmpty(source) {
305
+ const result = {};
306
+ for (const key in source) {
307
+ const value = source[key];
308
+ // Allow `0` and `false` but filter falsy values that indicate "empty"
309
+ if (value !== undefined && value !== null && value !== '') {
310
+ if (!Array.isArray(value) || value.length > 0) {
311
+ result[key] = source[key];
312
+ }
313
+ }
314
+ }
315
+ return result;
316
+ }
317
+
318
+ /**
319
+ * Sorts query params by both key and value returning a new URLSearchParams
320
+ * object with the keys inserted in sorted order.
321
+ *
322
+ * Treats `included` specially, splicing it into an array if it is a string and sorting the array.
323
+ *
324
+ * Options:
325
+ * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
326
+ *
327
+ * 'bracket': appends [] to the key for every value e.g. `&ids[]=1&ids[]=2`
328
+ * 'indices': appends [i] to the key for every value e.g. `&ids[0]=1&ids[1]=2`
329
+ * 'repeat': appends the key for every value e.g. `&ids=1&ids=2`
330
+ * 'comma' (default): appends the key once with a comma separated list of values e.g. `&ids=1,2`
331
+ *
332
+ * @method sortQueryParams
333
+ * @static
334
+ * @public
335
+ * @for @ember-data/request-utils
336
+ * @param {URLSearchParams | object} params
337
+ * @param {object} options
338
+ * @return {URLSearchParams} A URLSearchParams with keys inserted in sorted order
339
+ */
340
+ function sortQueryParams(params, options) {
341
+ const opts = Object.assign({}, DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS, options);
342
+ const paramsIsObject = !(params instanceof URLSearchParams);
343
+ const urlParams = new URLSearchParams();
344
+ const dictionaryParams = paramsIsObject ? params : {};
345
+ if (!paramsIsObject) {
346
+ params.forEach((value, key) => {
347
+ const hasExisting = key in dictionaryParams;
348
+ if (!hasExisting) {
349
+ dictionaryParams[key] = value;
350
+ } else {
351
+ const existingValue = dictionaryParams[key];
352
+ if (Array.isArray(existingValue)) {
353
+ existingValue.push(value);
354
+ } else {
355
+ dictionaryParams[key] = [existingValue, value];
356
+ }
357
+ }
358
+ });
359
+ }
360
+ if ('include' in dictionaryParams) {
361
+ dictionaryParams.include = handleInclude(dictionaryParams.include);
362
+ }
363
+ const sortedKeys = Object.keys(dictionaryParams).sort();
364
+ sortedKeys.forEach(key => {
365
+ const value = dictionaryParams[key];
366
+ if (Array.isArray(value)) {
367
+ value.sort();
368
+ switch (opts.arrayFormat) {
369
+ case 'indices':
370
+ value.forEach((v, i) => {
371
+ urlParams.append(`${key}[${i}]`, String(v));
372
+ });
373
+ return;
374
+ case 'bracket':
375
+ value.forEach(v => {
376
+ urlParams.append(`${key}[]`, String(v));
377
+ });
378
+ return;
379
+ case 'repeat':
380
+ value.forEach(v => {
381
+ urlParams.append(key, String(v));
382
+ });
383
+ return;
384
+ case 'comma':
385
+ default:
386
+ urlParams.append(key, value.join(','));
387
+ return;
388
+ }
389
+ } else {
390
+ urlParams.append(key, String(value));
391
+ }
392
+ });
393
+ return urlParams;
394
+ }
395
+
396
+ /**
397
+ * Sorts query params by both key and value, returning a query params string
398
+ *
399
+ * Treats `included` specially, splicing it into an array if it is a string and sorting the array.
400
+ *
401
+ * Options:
402
+ * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
403
+ *
404
+ * 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2`
405
+ * 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2`
406
+ * 'repeat': appends the key for every value e.g. `ids=1&ids=2`
407
+ * 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2`
408
+ *
409
+ * @method buildQueryParams
410
+ * @static
411
+ * @public
412
+ * @for @ember-data/request-utils
413
+ * @param {URLSearchParams | object} params
414
+ * @param {object} [options]
415
+ * @return {string} A sorted query params string without the leading `?`
416
+ */
417
+ function buildQueryParams(params, options) {
418
+ return sortQueryParams(params, options).toString();
419
+ }
420
+ const NUMERIC_KEYS = new Set(['max-age', 's-maxage', 'stale-if-error', 'stale-while-revalidate']);
421
+
422
+ /**
423
+ * Parses a string Cache-Control header value into an object with the following structure:
424
+ *
425
+ * ```ts
426
+ * interface CacheControlValue {
427
+ * immutable?: boolean;
428
+ * 'max-age'?: number;
429
+ * 'must-revalidate'?: boolean;
430
+ * 'must-understand'?: boolean;
431
+ * 'no-cache'?: boolean;
432
+ * 'no-store'?: boolean;
433
+ * 'no-transform'?: boolean;
434
+ * 'only-if-cached'?: boolean;
435
+ * private?: boolean;
436
+ * 'proxy-revalidate'?: boolean;
437
+ * public?: boolean;
438
+ * 's-maxage'?: number;
439
+ * 'stale-if-error'?: number;
440
+ * 'stale-while-revalidate'?: number;
441
+ * }
442
+ * ```
443
+ * @method parseCacheControl
444
+ * @static
445
+ * @public
446
+ * @for @ember-data/request-utils
447
+ * @param {string} header
448
+ * @return {CacheControlValue}
449
+ */
450
+ function parseCacheControl(header) {
451
+ let key = '';
452
+ let value = '';
453
+ let isParsingKey = true;
454
+ const cacheControlValue = {};
455
+ function parseCacheControlValue(stringToParse) {
456
+ const parsedValue = Number.parseInt(stringToParse);
457
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
458
+ if (!test) {
459
+ throw new Error(`Invalid Cache-Control value, expected a number but got - ${stringToParse}`);
460
+ }
461
+ })(!Number.isNaN(parsedValue)) : {};
462
+ return parsedValue;
463
+ }
464
+ for (let i = 0; i < header.length; i++) {
465
+ const char = header.charAt(i);
466
+ if (char === ',') {
467
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
468
+ if (!test) {
469
+ throw new Error(`Invalid Cache-Control value, expected a value`);
470
+ }
471
+ })(!isParsingKey || !NUMERIC_KEYS.has(key)) : {};
472
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
473
+ if (!test) {
474
+ throw new Error(`Invalid Cache-Control value, expected a value after "=" but got ","`);
475
+ }
476
+ })(i === 0 || header.charAt(i - 1) !== '=') : {};
477
+ isParsingKey = true;
478
+ // @ts-expect-error TS incorrectly thinks that optional keys must have a type that includes undefined
479
+ cacheControlValue[key] = NUMERIC_KEYS.has(key) ? parseCacheControlValue(value) : true;
480
+ key = '';
481
+ value = '';
482
+ continue;
483
+ } else if (char === '=') {
484
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
485
+ if (!test) {
486
+ throw new Error(`Invalid Cache-Control value, expected a value after "="`);
487
+ }
488
+ })(i + 1 !== header.length) : {};
489
+ isParsingKey = false;
490
+ } else if (char === ' ' || char === `\t` || char === `\n`) {
491
+ continue;
492
+ } else if (isParsingKey) {
493
+ key += char;
494
+ } else {
495
+ value += char;
496
+ }
497
+ if (i === header.length - 1) {
498
+ // @ts-expect-error TS incorrectly thinks that optional keys must have a type that includes undefined
499
+ cacheControlValue[key] = NUMERIC_KEYS.has(key) ? parseCacheControlValue(value) : true;
500
+ }
501
+ }
502
+ return cacheControlValue;
503
+ }
504
+ function isStale(headers, expirationTime) {
505
+ // const age = headers.get('age');
506
+ // const cacheControl = parseCacheControl(headers.get('cache-control') || '');
507
+ // const expires = headers.get('expires');
508
+ // const lastModified = headers.get('last-modified');
509
+ const date = headers.get('date');
510
+ if (!date) {
511
+ return true;
512
+ }
513
+ const time = new Date(date).getTime();
514
+ const now = Date.now();
515
+ const deadline = time + expirationTime;
516
+ const result = now > deadline;
517
+ return result;
518
+ }
519
+ /**
520
+ * A basic CachePolicy that can be added to the Store service.
521
+ *
522
+ * Determines staleness based on time since the request was last received from the API
523
+ * using the `date` header.
524
+ *
525
+ * Invalidates any request for which `cacheOptions.types` was provided when a createRecord
526
+ * request for that type is successful.
527
+ *
528
+ * For this to work, the `createRecord` request must include the `cacheOptions.types` array
529
+ * with the types that should be invalidated, or its request should specify the identifiers
530
+ * of the records that are being created via `records`. Providing both is valid.
531
+ *
532
+ * > [!NOTE]
533
+ * > only requests that had specified `cacheOptions.types` and occurred prior to the
534
+ * > createRecord request will be invalidated. This means that a given request should always
535
+ * > specify the types that would invalidate it to opt into this behavior. Abstracting this
536
+ * > behavior via builders is recommended to ensure consistency.
537
+ *
538
+ * This allows the Store's CacheHandler to determine if a request is expired and
539
+ * should be refetched upon next request.
540
+ *
541
+ * The `Fetch` handler provided by `@ember-data/request/fetch` will automatically
542
+ * add the `date` header to responses if it is not present.
543
+ *
544
+ * > [!NOTE]
545
+ * > Date headers do not have millisecond precision, so expiration times should
546
+ * > generally be larger than 1000ms.
547
+ *
548
+ * Usage:
549
+ *
550
+ * ```ts
551
+ * import { CachePolicy } from '@ember-data/request-utils';
552
+ * import DataStore from '@ember-data/store';
553
+ *
554
+ * // ...
555
+ *
556
+ * export class Store extends DataStore {
557
+ * constructor(args) {
558
+ * super(args);
559
+ * this.lifetimes = new CachePolicy({ apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 });
560
+ * }
561
+ * }
562
+ * ```
563
+ *
564
+ * @class CachePolicy
565
+ * @public
566
+ * @module @ember-data/request-utils
567
+ */
568
+ class CachePolicy {
569
+ _getStore(store) {
570
+ let set = this._stores.get(store);
571
+ if (!set) {
572
+ set = {
573
+ invalidated: new Set(),
574
+ types: new Map()
575
+ };
576
+ this._stores.set(store, set);
577
+ }
578
+ return set;
579
+ }
580
+ constructor(config) {
581
+ this._stores = new WeakMap();
582
+ const _config = arguments.length === 1 ? config : arguments[1];
583
+ deprecate(`Passing a Store to the CachePolicy is deprecated, please pass only a config instead.`, arguments.length === 1, {
584
+ id: 'ember-data:request-utils:lifetimes-service-store-arg',
585
+ since: {
586
+ enabled: '5.4',
587
+ available: '4.13'
588
+ },
589
+ for: '@ember-data/request-utils',
590
+ until: '6.0'
591
+ });
592
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
593
+ if (!test) {
594
+ throw new Error(`You must pass a config to the CachePolicy`);
595
+ }
596
+ })(_config) : {};
597
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
598
+ if (!test) {
599
+ throw new Error(`You must pass a apiCacheSoftExpires to the CachePolicy`);
600
+ }
601
+ })(typeof _config.apiCacheSoftExpires === 'number') : {};
602
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
603
+ if (!test) {
604
+ throw new Error(`You must pass a apiCacheHardExpires to the CachePolicy`);
605
+ }
606
+ })(typeof _config.apiCacheHardExpires === 'number') : {};
607
+ this.config = _config;
608
+ }
609
+
610
+ /**
611
+ * Invalidate a request by its identifier for a given store instance.
612
+ *
613
+ * While the store argument may seem redundant, the CachePolicy
614
+ * is designed to be shared across multiple stores / forks
615
+ * of the store.
616
+ *
617
+ * ```ts
618
+ * store.lifetimes.invalidateRequest(store, identifier);
619
+ * ```
620
+ *
621
+ * @method invalidateRequest
622
+ * @public
623
+ * @param {StableDocumentIdentifier} identifier
624
+ * @param {Store} store
625
+ */
626
+ invalidateRequest(identifier, store) {
627
+ this._getStore(store).invalidated.add(identifier);
628
+ }
629
+
630
+ /**
631
+ * Invalidate all requests associated to a specific type
632
+ * for a given store instance.
633
+ *
634
+ * While the store argument may seem redundant, the CachePolicy
635
+ * is designed to be shared across multiple stores / forks
636
+ * of the store.
637
+ *
638
+ * This invalidation is done automatically when using this service
639
+ * for both the CacheHandler and the LegacyNetworkHandler.
640
+ *
641
+ * ```ts
642
+ * store.lifetimes.invalidateRequestsForType(store, 'person');
643
+ * ```
644
+ *
645
+ * @method invalidateRequestsForType
646
+ * @public
647
+ * @param {string} type
648
+ * @param {Store} store
649
+ */
650
+ invalidateRequestsForType(type, store) {
651
+ const storeCache = this._getStore(store);
652
+ const set = storeCache.types.get(type);
653
+ const notifications = store.notifications;
654
+ if (set) {
655
+ // TODO batch notifications
656
+ set.forEach(id => {
657
+ storeCache.invalidated.add(id);
658
+ notifications.notify(id, 'invalidated');
659
+ });
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Invoked when a request has been fulfilled from the configured request handlers.
665
+ * This is invoked by the CacheHandler for both foreground and background requests
666
+ * once the cache has been updated.
667
+ *
668
+ * Note, this is invoked by the CacheHandler regardless of whether
669
+ * the request has a cache-key.
670
+ *
671
+ * This method should not be invoked directly by consumers.
672
+ *
673
+ * @method didRequest
674
+ * @public
675
+ * @param {ImmutableRequestInfo} request
676
+ * @param {ImmutableResponse} response
677
+ * @param {Store} store
678
+ * @param {StableDocumentIdentifier | null} identifier
679
+ * @return {void}
680
+ */
681
+ didRequest(request, response, identifier, store) {
682
+ // if this is a successful createRecord request, invalidate the cacheKey for the type
683
+ if (request.op === 'createRecord') {
684
+ const statusNumber = response?.status ?? 0;
685
+ if (statusNumber >= 200 && statusNumber < 400) {
686
+ const types = new Set(request.records?.map(r => r.type));
687
+ const additionalTypes = request.cacheOptions?.types;
688
+ additionalTypes?.forEach(type => {
689
+ types.add(type);
690
+ });
691
+ types.forEach(type => {
692
+ this.invalidateRequestsForType(type, store);
693
+ });
694
+ }
695
+
696
+ // add this document's cacheKey to a map for all associated types
697
+ // it is recommended to only use this for queries
698
+ } else if (identifier && request.cacheOptions?.types?.length) {
699
+ const storeCache = this._getStore(store);
700
+ request.cacheOptions?.types.forEach(type => {
701
+ const set = storeCache.types.get(type);
702
+ if (set) {
703
+ set.add(identifier);
704
+ storeCache.invalidated.delete(identifier);
705
+ } else {
706
+ storeCache.types.set(type, new Set([identifier]));
707
+ }
708
+ });
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Invoked to determine if the request may be fulfilled from cache
714
+ * if possible.
715
+ *
716
+ * Note, this is only invoked by the CacheHandler if the request has
717
+ * a cache-key.
718
+ *
719
+ * If no cache entry is found or the entry is hard expired,
720
+ * the request will be fulfilled from the configured request handlers
721
+ * and the cache will be updated before returning the response.
722
+ *
723
+ * @method isHardExpired
724
+ * @public
725
+ * @param {StableDocumentIdentifier} identifier
726
+ * @param {Store} store
727
+ * @return {boolean} true if the request is considered hard expired
728
+ */
729
+ isHardExpired(identifier, store) {
730
+ // if we are explicitly invalidated, we are hard expired
731
+ const storeCache = this._getStore(store);
732
+ if (storeCache.invalidated.has(identifier)) {
733
+ return true;
734
+ }
735
+ const cache = store.cache;
736
+ const cached = cache.peekRequest(identifier);
737
+ return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheHardExpires);
738
+ }
739
+
740
+ /**
741
+ * Invoked if `isHardExpired` is false to determine if the request
742
+ * should be update behind the scenes if cache data is already available.
743
+ *
744
+ * Note, this is only invoked by the CacheHandler if the request has
745
+ * a cache-key.
746
+ *
747
+ * If true, the request will be fulfilled from cache while a backgrounded
748
+ * request is made to update the cache via the configured request handlers.
749
+ *
750
+ * @method isSoftExpired
751
+ * @public
752
+ * @param {StableDocumentIdentifier} identifier
753
+ * @param {Store} store
754
+ * @return {boolean} true if the request is considered soft expired
755
+ */
756
+ isSoftExpired(identifier, store) {
757
+ const cache = store.cache;
758
+ const cached = cache.peekRequest(identifier);
759
+ return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheSoftExpires);
760
+ }
761
+ }
762
+ class LifetimesService extends CachePolicy {
763
+ constructor(config) {
764
+ deprecate(`\`import { LifetimesService } from '@ember-data/request-utils';\` is deprecated, please use \`import { CachePolicy } from '@ember-data/request-utils';\` instead.`, false, {
765
+ id: 'ember-data:deprecate-lifetimes-service-import',
766
+ since: {
767
+ enabled: '5.4',
768
+ available: '4.13'
769
+ },
770
+ for: 'ember-data',
771
+ until: '6.0'
772
+ });
773
+ super(config);
774
+ }
775
+ }
776
+ export { CachePolicy, LifetimesService, buildBaseURL, buildQueryParams, filterEmpty, parseCacheControl, setBuildURLConfig, sortQueryParams };