@ember-data/store 4.7.0-beta.1 → 4.8.0-alpha.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.
@@ -1788,7 +1788,10 @@ class Store extends Service {
1788
1788
  if (DEBUG) {
1789
1789
  assertDestroyingStore(this, 'recordWasInvalid');
1790
1790
  }
1791
- internalModel.adapterDidInvalidate(parsedErrors, error);
1791
+ error = error || new Error(`unknown invalid error`);
1792
+ error = typeof error === 'string' ? new Error(error) : error;
1793
+ error._parsedErrors = parsedErrors;
1794
+ internalModel.adapterDidInvalidate(error);
1792
1795
  }
1793
1796
  assert(`store.recordWasInvalid has been removed`);
1794
1797
  }
@@ -2182,7 +2185,8 @@ class Store extends Service {
2182
2185
 
2183
2186
  //We first make sure the primary data has been updated
2184
2187
  //TODO try to move notification to the user to the end of the runloop
2185
- internalModel.adapterDidCommit(data);
2188
+ internalModel._recordData.didCommit(data);
2189
+ this.recordArrayManager.recordDidChange(internalModel.identifier);
2186
2190
 
2187
2191
  if (payload && payload.included) {
2188
2192
  this._push({ data: null, included: payload.included });
@@ -2191,12 +2195,14 @@ class Store extends Service {
2191
2195
  return record;
2192
2196
  },
2193
2197
  (e) => {
2194
- if (typeof e === 'string') {
2195
- throw e;
2198
+ let err = e;
2199
+ if (!e) {
2200
+ err = new Error(`Unknown Error Occurred During Request`);
2201
+ } else if (typeof e === 'string') {
2202
+ err = new Error(e);
2196
2203
  }
2197
- const { error, parsedErrors } = e;
2198
- internalModel.adapterDidInvalidate(parsedErrors, error);
2199
- throw error;
2204
+ internalModel.adapterDidInvalidate(err);
2205
+ throw err;
2200
2206
  }
2201
2207
  );
2202
2208
  }
@@ -22,7 +22,6 @@ import type { Dict } from '@ember-data/types/q/utils';
22
22
  import coerceId from './coerce-id';
23
23
  import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common';
24
24
  import type Store from './core-store';
25
- import { errorsArrayToHash } from './errors-utils';
26
25
  import ShimModelClass from './model/shim-model-class';
27
26
  import RequestCache from './request-cache';
28
27
  import { normalizeResponseHelper } from './serializer-response';
@@ -170,24 +169,7 @@ export default class FetchManager {
170
169
  if (adapterPayload) {
171
170
  return normalizeResponseHelper(serializer, store, modelClass, adapterPayload, snapshot.id, operation);
172
171
  }
173
- },
174
- function (error) {
175
- if (error && error.isAdapterError === true && error.code === 'InvalidError') {
176
- let parsedErrors = error.errors;
177
-
178
- // TODO deprecate extractErrors being called and/or make it part of the public interface
179
- if (serializer && typeof serializer.extractErrors === 'function') {
180
- parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id);
181
- } else {
182
- parsedErrors = errorsArrayToHash(error.errors);
183
- }
184
-
185
- throw { error, parsedErrors };
186
- } else {
187
- throw { error };
188
- }
189
- },
190
- label
172
+ }
191
173
  );
192
174
  resolver.resolve(promise);
193
175
  }
@@ -326,38 +308,32 @@ export default class FetchManager {
326
308
  }),
327
309
  this._store,
328
310
  label
329
- ).then(
330
- (adapterPayload) => {
331
- assert(
332
- `You made a 'findRecord' request for a '${modelName}' with id '${id}', but the adapter's response did not have any data`,
333
- !!payloadIsNotBlank(adapterPayload)
334
- );
335
- let serializer = this._store.serializerFor(modelName);
336
- let payload = normalizeResponseHelper(serializer, this._store, klass, adapterPayload, id, 'findRecord');
337
- assert(
338
- `Ember Data expected the primary data returned from a 'findRecord' response to be an object but instead it found an array.`,
339
- !Array.isArray(payload.data)
340
- );
341
- assert(
342
- `The 'findRecord' request for ${modelName}:${id} resolved indicating success but contained no primary data. To indicate a 404 not found you should either reject the promise returned by the adapter's findRecord method or throw a NotFoundError.`,
343
- 'data' in payload && payload.data !== null && typeof payload.data === 'object'
344
- );
311
+ ).then((adapterPayload) => {
312
+ assert(
313
+ `You made a 'findRecord' request for a '${modelName}' with id '${id}', but the adapter's response did not have any data`,
314
+ !!payloadIsNotBlank(adapterPayload)
315
+ );
316
+ let serializer = this._store.serializerFor(modelName);
317
+ let payload = normalizeResponseHelper(serializer, this._store, klass, adapterPayload, id, 'findRecord');
318
+ assert(
319
+ `Ember Data expected the primary data returned from a 'findRecord' response to be an object but instead it found an array.`,
320
+ !Array.isArray(payload.data)
321
+ );
322
+ assert(
323
+ `The 'findRecord' request for ${modelName}:${id} resolved indicating success but contained no primary data. To indicate a 404 not found you should either reject the promise returned by the adapter's findRecord method or throw a NotFoundError.`,
324
+ 'data' in payload && payload.data !== null && typeof payload.data === 'object'
325
+ );
345
326
 
346
- warn(
347
- `You requested a record of type '${modelName}' with id '${id}' but the adapter returned a payload with primary data having an id of '${payload.data.id}'. Use 'store.findRecord()' when the requested id is the same as the one returned by the adapter. In other cases use 'store.queryRecord()' instead.`,
348
- coerceId(payload.data.id) === coerceId(id),
349
- {
350
- id: 'ds.store.findRecord.id-mismatch',
351
- }
352
- );
327
+ warn(
328
+ `You requested a record of type '${modelName}' with id '${id}' but the adapter returned a payload with primary data having an id of '${payload.data.id}'. Use 'store.findRecord()' when the requested id is the same as the one returned by the adapter. In other cases use 'store.queryRecord()' instead.`,
329
+ coerceId(payload.data.id) === coerceId(id),
330
+ {
331
+ id: 'ds.store.findRecord.id-mismatch',
332
+ }
333
+ );
353
334
 
354
- return payload;
355
- },
356
- (error) => {
357
- throw error;
358
- },
359
- `DS: Extract payload of '${modelName}'`
360
- );
335
+ return payload;
336
+ });
361
337
 
362
338
  fetchItem.resolver.resolve(promise);
363
339
  }
@@ -2,6 +2,12 @@
2
2
  @module @ember-data/store
3
3
  */
4
4
 
5
+ import { assert, deprecate } from '@ember/debug';
6
+
7
+ import { DEPRECATE_HELPERS } from '@ember-data/private-build-infra/deprecations';
8
+
9
+ import _normalize from './normalize-model-name';
10
+
5
11
  export { default as Store, storeFor } from './core-store';
6
12
 
7
13
  export { recordIdentifierFor } from './internal-model-factory';
@@ -14,10 +20,24 @@ export {
14
20
  setIdentifierResetMethod,
15
21
  } from './identifier-cache';
16
22
 
17
- export { default as normalizeModelName } from './normalize-model-name';
18
- export { default as coerceId } from './coerce-id';
23
+ export function normalizeModelName(modelName: string) {
24
+ if (DEPRECATE_HELPERS) {
25
+ deprecate(
26
+ `the helper function normalizeModelName is deprecated. You should use model names that are already normalized, or use string helpers of your own. This function is primarily an alias for dasherize from @ember/string.`,
27
+ false,
28
+ {
29
+ id: 'ember-data:deprecate-normalize-modelname-helper',
30
+ for: 'ember-data',
31
+ until: '5.0',
32
+ since: { available: '4.8', enabled: '4.8' },
33
+ }
34
+ );
35
+ return _normalize(modelName);
36
+ }
37
+ assert(`normalizeModelName support has been removed`);
38
+ }
19
39
 
20
- export { errorsHashToArray, errorsArrayToHash } from './errors-utils';
40
+ export { default as coerceId } from './coerce-id';
21
41
 
22
42
  // `ember-data-model-fragments` relies on `InternalModel`
23
43
  export { default as InternalModel } from './model/internal-model';
@@ -5,13 +5,14 @@ import { DEBUG } from '@glimmer/env';
5
5
  import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra';
6
6
  import type { DSModel } from '@ember-data/types/q/ds-model';
7
7
  import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
8
+ import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface';
8
9
  import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data';
9
10
  import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api';
10
11
  import type { RecordInstance } from '@ember-data/types/q/record-instance';
11
12
 
12
13
  import type Store from '../core-store';
13
- import { errorsHashToArray } from '../errors-utils';
14
14
  import { internalModelFactoryFor } from '../internal-model-factory';
15
+ import type ShimModelClass from './shim-model-class';
15
16
 
16
17
  /**
17
18
  @module @ember-data/store
@@ -27,6 +28,11 @@ function isDSModel(record: RecordInstance | null): record is DSModel {
27
28
  );
28
29
  }
29
30
 
31
+ type AdapterErrors = Error & { errors?: unknown[]; isAdapterError?: true; code?: string };
32
+ type SerializerWithParseErrors = MinimumSerializerInterface & {
33
+ extractErrors?(store: Store, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): any;
34
+ };
35
+
30
36
  export default class InternalModel {
31
37
  declare _id: string | null;
32
38
  declare modelName: string;
@@ -91,7 +97,10 @@ export default class InternalModel {
91
97
  let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value };
92
98
  // TODO potentially this needs to handle merged result
93
99
  this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier);
94
- this.notifyPropertyChange('id');
100
+ if (this.hasRecord) {
101
+ // TODO this should likely *mostly* be the a different bucket
102
+ this.store._notificationManager.notify(this.identifier, 'property', 'id');
103
+ }
95
104
  }
96
105
  }
97
106
 
@@ -392,16 +401,6 @@ export default class InternalModel {
392
401
  }
393
402
  }
394
403
 
395
- notifyPropertyChange(key: string) {
396
- if (this.hasRecord) {
397
- // TODO this should likely *mostly* be the `attributes` bucket
398
- // but it seems for local mutations we rely on computed updating
399
- // iteself when set. As we design our own thing we may need to change
400
- // that.
401
- this.store._notificationManager.notify(this.identifier, 'property', key);
402
- }
403
- }
404
-
405
404
  notifyStateChange(key?: string) {
406
405
  if (this.hasRecord) {
407
406
  this.store._notificationManager.notify(this.identifier, 'state');
@@ -533,52 +532,26 @@ export default class InternalModel {
533
532
  this._isUpdatingId = false;
534
533
  }
535
534
 
536
- didError() {}
537
-
538
- /*
539
- If the adapter did not return a hash in response to a commit,
540
- merge the changed attributes and relationships into the existing
541
- saved data.
542
- */
543
- adapterDidCommit(data) {
544
- this._recordData.didCommit(data);
545
- this.store.recordArrayManager.recordDidChange(this.identifier);
546
- }
547
-
548
- hasErrors(): boolean {
549
- // TODO add assertion forcing consuming RecordData's to implement getErrors
550
- if (this._recordData.getErrors) {
551
- return this._recordData.getErrors(this.identifier).length > 0;
552
- } else {
553
- let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
554
- // we can't have errors if we never tried loading
555
- if (!record) {
556
- return false;
535
+ // FOR USE DURING COMMIT PROCESS
536
+ adapterDidInvalidate(error: Error & { errors?: JsonApiValidationError[]; isAdapterError?: true; code?: string }) {
537
+ if (error && error.isAdapterError === true && error.code === 'InvalidError') {
538
+ let serializer = this.store.serializerFor(this.modelName) as SerializerWithParseErrors;
539
+
540
+ // TODO @deprecate extractErrors being called
541
+ // TODO remove extractErrors from the default serializers.
542
+ if (serializer && typeof serializer.extractErrors === 'function') {
543
+ let errorsHash = serializer.extractErrors(this.store, this.store.modelFor(this.modelName), error, this.id);
544
+ error.errors = errorsHashToArray(errorsHash);
557
545
  }
558
- let errors = (record as DSModel).errors;
559
- return errors.length > 0;
560
546
  }
561
- }
562
547
 
563
- // FOR USE DURING COMMIT PROCESS
564
- adapterDidInvalidate(parsedErrors, error?) {
565
- // TODO @runspired this should be handled by RecordState
566
- // and errors should be dirtied but lazily fetch if at
567
- // all possible. We should only notify errors here.
568
- let attribute;
569
- if (error && parsedErrors) {
570
- // TODO add assertion forcing consuming RecordData's to implement getErrors
571
- if (!this._recordData.getErrors) {
572
- let record = this.store._instanceCache.getRecord(this.identifier) as DSModel;
573
- let errors = record.errors;
574
- for (attribute in parsedErrors) {
575
- if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) {
576
- errors.add(attribute, parsedErrors[attribute]);
577
- }
578
- }
579
- }
548
+ if (error.errors) {
549
+ assert(
550
+ `Expected the RecordData implementation for ${this.identifier} to have a getErrors(identifier) method for retreiving errors.`,
551
+ typeof this._recordData.getErrors === 'function'
552
+ );
580
553
 
581
- let jsonApiErrors: JsonApiValidationError[] = errorsHashToArray(parsedErrors);
554
+ let jsonApiErrors: JsonApiValidationError[] = error.errors;
582
555
  if (jsonApiErrors.length === 0) {
583
556
  jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }];
584
557
  }
@@ -588,15 +561,40 @@ export default class InternalModel {
588
561
  }
589
562
  }
590
563
 
591
- notifyErrorsChange() {
592
- this.store._notificationManager.notify(this.identifier, 'errors');
564
+ toString() {
565
+ return `<${this.modelName}:${this.id}>`;
593
566
  }
567
+ }
594
568
 
595
- adapterDidError() {
596
- this._recordData.commitWasRejected();
597
- }
569
+ function makeArray(value) {
570
+ return Array.isArray(value) ? value : [value];
571
+ }
598
572
 
599
- toString() {
600
- return `<${this.modelName}:${this.id}>`;
573
+ const PRIMARY_ATTRIBUTE_KEY = 'base';
574
+
575
+ function errorsHashToArray(errors): JsonApiValidationError[] {
576
+ const out: JsonApiValidationError[] = [];
577
+
578
+ if (errors) {
579
+ Object.keys(errors).forEach((key) => {
580
+ let messages = makeArray(errors[key]);
581
+ for (let i = 0; i < messages.length; i++) {
582
+ let title = 'Invalid Attribute';
583
+ let pointer = `/data/attributes/${key}`;
584
+ if (key === PRIMARY_ATTRIBUTE_KEY) {
585
+ title = 'Invalid Document';
586
+ pointer = `/data`;
587
+ }
588
+ out.push({
589
+ title: title,
590
+ detail: messages[i],
591
+ source: {
592
+ pointer: pointer,
593
+ },
594
+ });
595
+ }
596
+ });
601
597
  }
598
+
599
+ return out;
602
600
  }
@@ -4,9 +4,6 @@ import { dasherize } from '@ember/string';
4
4
  @module @ember-data/store
5
5
  */
6
6
 
7
- // All modelNames are dasherized internally. Changing this function may
8
- // require changes to other normalization hooks (such as typeForRoot).
9
-
10
7
  /**
11
8
  This method normalizes a modelName into the format Ember Data uses
12
9
  internally by dasherizing it.
@@ -14,6 +11,7 @@ import { dasherize } from '@ember/string';
14
11
  @method normalizeModelName
15
12
  @static
16
13
  @public
14
+ @deprecated
17
15
  @for @ember-data/store
18
16
  @param {String} modelName
19
17
  @return {String} normalizedModelName
@@ -75,11 +75,7 @@ export default class RecordDataStoreWrapper implements StoreWrapper {
75
75
  const resource = constructResource(type, id, lid);
76
76
  const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource);
77
77
 
78
- let internalModel = internalModelFactoryFor(this._store).peek(identifier);
79
-
80
- if (internalModel) {
81
- internalModel.notifyErrorsChange();
82
- }
78
+ this._store._notificationManager.notify(identifier, 'errors');
83
79
  }
84
80
 
85
81
  _flushNotifications(): void {
@@ -62,7 +62,7 @@ export default class RequestCache {
62
62
  state: 'rejected',
63
63
  request: queryRequest,
64
64
  type,
65
- response: { data: error && error.error },
65
+ response: { data: error },
66
66
  } as InternalRequest;
67
67
  finalizedRequest[Touching] = request[Touching];
68
68
  this._addDone(finalizedRequest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ember-data/store",
3
- "version": "4.7.0-beta.1",
3
+ "version": "4.8.0-alpha.2",
4
4
  "description": "The default blueprint for ember-cli addons.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -17,8 +17,8 @@
17
17
  "start": "ember serve"
18
18
  },
19
19
  "dependencies": {
20
- "@ember-data/canary-features": "4.7.0-beta.1",
21
- "@ember-data/private-build-infra": "4.7.0-beta.1",
20
+ "@ember-data/canary-features": "4.8.0-alpha.2",
21
+ "@ember-data/private-build-infra": "4.8.0-alpha.2",
22
22
  "@ember/string": "^3.0.0",
23
23
  "@embroider/macros": "^1.8.3",
24
24
  "@glimmer/tracking": "^1.1.2",
@@ -29,7 +29,7 @@
29
29
  "ember-cli-typescript": "^5.1.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@ember-data/unpublished-test-infra": "4.7.0-beta.1",
32
+ "@ember-data/unpublished-test-infra": "4.8.0-alpha.2",
33
33
  "@ember/optional-features": "^2.0.0",
34
34
  "@ember/test-helpers": "~2.7.0",
35
35
  "@types/ember": "^4.0.0",
@@ -1,146 +0,0 @@
1
- /**
2
- @module @ember-data/adapter/error
3
- */
4
-
5
- const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/;
6
- const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/;
7
- const PRIMARY_ATTRIBUTE_KEY = 'base';
8
-
9
- function makeArray(value) {
10
- return Array.isArray(value) ? value : [value];
11
- }
12
-
13
- /**
14
- Convert an hash of errors into an array with errors in JSON-API format.
15
- ```javascript
16
- import DS from 'ember-data';
17
-
18
- const { errorsHashToArray } = DS;
19
-
20
- let errors = {
21
- base: 'Invalid attributes on saving this record',
22
- name: 'Must be present',
23
- age: ['Must be present', 'Must be a number']
24
- };
25
- let errorsArray = errorsHashToArray(errors);
26
- // [
27
- // {
28
- // title: "Invalid Document",
29
- // detail: "Invalid attributes on saving this record",
30
- // source: { pointer: "/data" }
31
- // },
32
- // {
33
- // title: "Invalid Attribute",
34
- // detail: "Must be present",
35
- // source: { pointer: "/data/attributes/name" }
36
- // },
37
- // {
38
- // title: "Invalid Attribute",
39
- // detail: "Must be present",
40
- // source: { pointer: "/data/attributes/age" }
41
- // },
42
- // {
43
- // title: "Invalid Attribute",
44
- // detail: "Must be a number",
45
- // source: { pointer: "/data/attributes/age" }
46
- // }
47
- // ]
48
- ```
49
- @method errorsHashToArray
50
- @for @ember-data/adapter/error
51
- @static
52
- @public
53
- @param {Object} errors hash with errors as properties
54
- @return {Array} array of errors in JSON-API format
55
- */
56
- export function errorsHashToArray(errors) {
57
- let out = [];
58
-
59
- if (errors) {
60
- Object.keys(errors).forEach((key) => {
61
- let messages = makeArray(errors[key]);
62
- for (let i = 0; i < messages.length; i++) {
63
- let title = 'Invalid Attribute';
64
- let pointer = `/data/attributes/${key}`;
65
- if (key === PRIMARY_ATTRIBUTE_KEY) {
66
- title = 'Invalid Document';
67
- pointer = `/data`;
68
- }
69
- out.push({
70
- title: title,
71
- detail: messages[i],
72
- source: {
73
- pointer: pointer,
74
- },
75
- });
76
- }
77
- });
78
- }
79
-
80
- return out;
81
- }
82
-
83
- /**
84
- Convert an array of errors in JSON-API format into an object.
85
-
86
- ```javascript
87
- import DS from 'ember-data';
88
-
89
- const { errorsArrayToHash } = DS;
90
-
91
- let errorsArray = [
92
- {
93
- title: 'Invalid Attribute',
94
- detail: 'Must be present',
95
- source: { pointer: '/data/attributes/name' }
96
- },
97
- {
98
- title: 'Invalid Attribute',
99
- detail: 'Must be present',
100
- source: { pointer: '/data/attributes/age' }
101
- },
102
- {
103
- title: 'Invalid Attribute',
104
- detail: 'Must be a number',
105
- source: { pointer: '/data/attributes/age' }
106
- }
107
- ];
108
-
109
- let errors = errorsArrayToHash(errorsArray);
110
- // {
111
- // "name": ["Must be present"],
112
- // "age": ["Must be present", "must be a number"]
113
- // }
114
- ```
115
-
116
- @method errorsArrayToHash
117
- @static
118
- @for @ember-data/adapter/error
119
- @public
120
- @param {Array} errors array of errors in JSON-API format
121
- @return {Object}
122
- */
123
- export function errorsArrayToHash(errors) {
124
- let out = {};
125
-
126
- if (errors) {
127
- errors.forEach((error) => {
128
- if (error.source && error.source.pointer) {
129
- let key = error.source.pointer.match(SOURCE_POINTER_REGEXP);
130
-
131
- if (key) {
132
- key = key[2];
133
- } else if (error.source.pointer.search(SOURCE_POINTER_PRIMARY_REGEXP) !== -1) {
134
- key = PRIMARY_ATTRIBUTE_KEY;
135
- }
136
-
137
- if (key) {
138
- out[key] = out[key] || [];
139
- out[key].push(error.detail || error.title);
140
- }
141
- }
142
- });
143
- }
144
-
145
- return out;
146
- }