From 1e7c5738b2866da41c00de31711f07e156193afd Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Tue, 24 Sep 2019 17:38:57 -0700 Subject: [PATCH] [DOC serializer] implements MinimumSerializerInterface (#6451) --- .../node-tests/fixtures/expected.js | 527 +++++++++--------- .../integration/identifiers/scenarios-test.ts | 8 +- packages/adapter/addon/-private/index.js | 1 + .../-private/utils/serialize-into-hash.js | 11 + packages/adapter/addon/json-api.js | 7 +- packages/adapter/addon/rest.js | 14 +- .../store/addon/-private/identifiers/cache.ts | 6 +- .../store/addon/-private/system/core-store.ts | 76 ++- .../addon/-private/system/fetch-manager.ts | 11 +- .../addon/-private/system/identity-map.ts | 4 +- .../-private/system/internal-model-map.ts | 8 +- .../-private/system/model/internal-model.ts | 10 +- .../ts-interfaces/ember-data-json-api.ts | 12 +- .../minimum-serializer-interface.ts | 319 +++++++++++ .../addon/-private/ts-interfaces/utils.ts | 3 +- 15 files changed, 686 insertions(+), 331 deletions(-) create mode 100644 packages/adapter/addon/-private/utils/serialize-into-hash.js create mode 100644 packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index 0fe1dca017d..63d4b26ffab 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -1,263 +1,266 @@ module.exports = { - "classitems": [ - "buildURL", - "_buildURL", - "urlForFindRecord", - "urlForFindAll", - "urlForQuery", - "urlForQueryRecord", - "urlForFindMany", - "urlForFindHasMany", - "urlForFindBelongsTo", - "urlForCreateRecord", - "urlForUpdateRecord", - "urlForDeleteRecord", - "urlPrefix", - "pathForType", - "errorsHashToArray", - "errorsArrayToHash", - "_registerHandlers", - "errorsByAttributeName", - "errorsFor", - "messages", - "content", - "unknownProperty", - "length", - "isEmpty", - "add", - "_add", - "_findOrCreateMessages", - "remove", - "_remove", - "_clear", - "has", - "isLoading", - "isLoaded", - "hasDirtyAttributes", - "isSaving", - "isDeleted", - "isNew", - "isValid", - "dirtyType", - "isError", - "isReloading", - "id", - "currentState", - "_internalModel", - "recordData", - "store", - "errors", - "adapterError", - "serialize", - "toJSON", - "ready", - "didLoad", - "didUpdate", - "didCreate", - "didDelete", - "becameInvalid", - "becameError", - "rolledBack", - "send", - "transitionTo", - "deleteRecord", - "destroyRecord", - "unloadRecord", - "_notifyProperties", - "changedAttributes", - "rollbackAttributes", - "save", - "reload", - "trigger", - "belongsTo", - "hasMany", - "_debugInfo", - "eachRelationship", - "data", - "create", - "modelName", - "typeForRelationship", - "inverseFor", - "relationships", - "relationshipNames", - "relatedTypes", - "relationshipsByName", - "fields", - "eachRelatedType", - "attributes", - "transformedAttributes", - "eachAttribute", - "eachTransformedAttribute", - "toString", - "_setInternalModels", - "isUpdating", - "type", - "objectAtContent", - "update", - "_pushInternalModels", - "removeInternalModel", - "_unregisterFromManager", - "push", - "value", - "load", - "remoteType", - "ids", - "link", - "meta", - "diffArray", - "retrieve", - "clear", - "get", - "models", - "metadata", - "promise", - "isPolymorphic", - "relationship", - "createRecord", - "normalizeModelName", - "liveRecordArrayFor", - "createRecordArray", - "createAdapterPopulatedRecordArray", - "unregisterRecordArray", - "_snapshots", - "_recordArray", - "adapterOptions", - "include", - "snapshots", - "record", - "attr", - "init", - "adapter", - "defaultAdapter", - "_generateId", - "find", - "findRecord", - "findByIds", - "_fetchRecord", - "getReference", - "peekRecord", - "_reloadRecord", - "hasRecordForId", - "recordForId", - "findMany", - "findHasMany", - "findBelongsTo", - "query", - "queryRecord", - "findAll", - "_fetchAll", - "_didUpdateAll", - "peekAll", - "unloadAll", - "scheduleSave", - "flushPendingSave", - "didSaveRecord", - "recordWasInvalid", - "recordWasError", - "setRecordId", - "_load", - "modelFor", - "pushPayload", - "normalize", - "adapterFor", - "serializerFor", - "VERSION", - "ajaxOptions", - "coalesceFindRequests", - "sortQueryParams", - "namespace", - "host", - "headers", - "updateRecord", - "groupRecordsForFindMany", - "handleResponse", - "isSuccess", - "isInvalid", - "ajax", - "_ajaxRequest", - "_najaxRequest", - "parseErrorResponse", - "normalizeErrorResponse", - "generatedDetailedMessage", - "serializeBelongsTo", - "serializeHasMany", - "removeEmbeddedForeignKey", - "_extractEmbeddedRecords", - "_extractEmbeddedHasMany", - "_extractEmbeddedBelongsTo", - "_normalizeEmbeddedRelationship", - "_normalizeDocumentHelper", - "_normalizeRelationshipDataHelper", - "_normalizeResourceHelper", - "_normalizeResponse", - "extractRelationship", - "extractRelationships", - "_extractType", - "modelNameFromPayloadKey", - "payloadKeyFromModelName", - "keyForAttribute", - "keyForRelationship", - "primaryKey", - "attrs", - "applyTransforms", - "normalizeResponse", - "normalizeFindRecordResponse", - "normalizeQueryRecordResponse", - "normalizeFindAllResponse", - "normalizeFindBelongsToResponse", - "normalizeFindHasManyResponse", - "normalizeFindManyResponse", - "normalizeQueryResponse", - "normalizeCreateRecordResponse", - "normalizeDeleteRecordResponse", - "normalizeUpdateRecordResponse", - "normalizeSaveResponse", - "normalizeSingleResponse", - "normalizeArrayResponse", - "extractId", - "extractAttributes", - "extractPolymorphicRelationship", - "normalizeRelationships", - "normalizeUsingDeclaredMapping", - "_getMappedKey", - "_canSerialize", - "_mustSerialize", - "shouldSerializeHasMany", - "serializeIntoHash", - "serializeAttribute", - "serializePolymorphicType", - "extractMeta", - "extractErrors", - "keyForLink", - "transformFor", - "keyForPolymorphicType", - "_normalizeArray", - "deserialize", - "defaultSerializer", - "generateIdForRecord", - "shouldReloadRecord", - "shouldReloadAll", - "shouldBackgroundReloadRecord", - "shouldBackgroundReloadAll", - "pluralize", - "singularize", - "enableCache", - "purgedCache", - "disableCache;", - "plural", - "singular", - "uncountable", - "irregular", - "inflect", - "columnNameToDesc", - "columnsForType", - "detect", - "getFilters", - "getRecordColor", - "getRecordColumnValues", - "getRecordFilterValues", - "getRecordKeywords", - "getRecords", - "observerRecord" - ] -} + classitems: [ + 'buildURL', + '_buildURL', + 'urlForFindRecord', + 'urlForFindAll', + 'urlForQuery', + 'urlForQueryRecord', + 'urlForFindMany', + 'urlForFindHasMany', + 'urlForFindBelongsTo', + 'urlForCreateRecord', + 'urlForUpdateRecord', + 'urlForDeleteRecord', + 'urlPrefix', + 'pathForType', + 'errorsHashToArray', + 'errorsArrayToHash', + '_registerHandlers', + 'errorsByAttributeName', + 'errorsFor', + 'messages', + 'content', + 'unknownProperty', + 'length', + 'isEmpty', + 'add', + '_add', + '_findOrCreateMessages', + 'remove', + '_remove', + '_clear', + 'has', + 'isLoading', + 'isLoaded', + 'hasDirtyAttributes', + 'isSaving', + 'isDeleted', + 'isNew', + 'isValid', + 'dirtyType', + 'isError', + 'isReloading', + 'id', + 'currentState', + '_internalModel', + 'recordData', + 'store', + 'errors', + 'adapterError', + 'serialize', + 'toJSON', + 'ready', + 'didLoad', + 'didUpdate', + 'didCreate', + 'didDelete', + 'becameInvalid', + 'becameError', + 'rolledBack', + 'send', + 'transitionTo', + 'deleteRecord', + 'destroyRecord', + 'unloadRecord', + '_notifyProperties', + 'changedAttributes', + 'rollbackAttributes', + 'save', + 'reload', + 'trigger', + 'belongsTo', + 'hasMany', + '_debugInfo', + 'eachRelationship', + 'data', + 'create', + 'modelName', + 'typeForRelationship', + 'inverseFor', + 'relationships', + 'relationshipNames', + 'relatedTypes', + 'relationshipsByName', + 'fields', + 'eachRelatedType', + 'attributes', + 'transformedAttributes', + 'eachAttribute', + 'eachTransformedAttribute', + 'toString', + '_setInternalModels', + 'isUpdating', + 'type', + 'objectAtContent', + 'update', + '_pushInternalModels', + 'removeInternalModel', + '_unregisterFromManager', + 'push', + 'value', + 'load', + 'remoteType', + 'ids', + 'link', + 'meta', + 'diffArray', + 'retrieve', + 'clear', + 'get', + 'models', + 'metadata', + 'promise', + 'isPolymorphic', + 'relationship', + 'createRecord', + 'normalizeModelName', + 'liveRecordArrayFor', + 'createRecordArray', + 'createAdapterPopulatedRecordArray', + 'unregisterRecordArray', + '_snapshots', + '_recordArray', + 'adapterOptions', + 'include', + 'snapshots', + 'record', + 'attr', + 'init', + 'adapter', + 'defaultAdapter', + '_generateId', + 'find', + 'findRecord', + 'findByIds', + '_fetchRecord', + 'getReference', + 'peekRecord', + '_reloadRecord', + 'hasRecordForId', + 'recordForId', + 'findMany', + 'findHasMany', + 'findBelongsTo', + 'query', + 'queryRecord', + 'findAll', + '_fetchAll', + '_didUpdateAll', + 'peekAll', + 'unloadAll', + 'scheduleSave', + 'flushPendingSave', + 'didSaveRecord', + 'recordWasInvalid', + 'recordWasError', + 'setRecordId', + '_load', + 'modelFor', + 'pushPayload', + 'normalize', + 'adapterFor', + 'serializerFor', + 'VERSION', + 'ajaxOptions', + 'coalesceFindRequests', + 'sortQueryParams', + 'namespace', + 'host', + 'headers', + 'updateRecord', + 'groupRecordsForFindMany', + 'handleResponse', + 'isSuccess', + 'isInvalid', + 'ajax', + '_ajaxRequest', + '_najaxRequest', + 'parseErrorResponse', + 'normalizeErrorResponse', + 'generatedDetailedMessage', + 'serializeBelongsTo', + 'serializeHasMany', + 'removeEmbeddedForeignKey', + '_extractEmbeddedRecords', + '_extractEmbeddedHasMany', + '_extractEmbeddedBelongsTo', + '_normalizeEmbeddedRelationship', + '_normalizeDocumentHelper', + '_normalizeRelationshipDataHelper', + '_normalizeResourceHelper', + '_normalizeResponse', + 'extractRelationship', + 'extractRelationships', + '_extractType', + 'modelNameFromPayloadKey', + 'payloadKeyFromModelName', + 'keyForAttribute', + 'keyForRelationship', + 'primaryKey', + 'attrs', + 'applyTransforms', + 'normalizeResponse', + 'normalizeFindRecordResponse', + 'normalizeQueryRecordResponse', + 'normalizeFindAllResponse', + 'normalizeFindBelongsToResponse', + 'normalizeFindHasManyResponse', + 'normalizeFindManyResponse', + 'normalizeQueryResponse', + 'normalizeCreateRecordResponse', + 'normalizeDeleteRecordResponse', + 'normalizeUpdateRecordResponse', + 'normalizeSaveResponse', + 'normalizeSingleResponse', + 'normalizeArrayResponse', + 'extractId', + 'extractAttributes', + 'extractPolymorphicRelationship', + 'normalizeRelationships', + 'normalizeUsingDeclaredMapping', + '_getMappedKey', + '_canSerialize', + '_mustSerialize', + 'shouldSerializeHasMany', + 'serializeIntoHash', + 'serializeAttribute', + 'serializePolymorphicType', + 'extractMeta', + 'extractErrors', + 'keyForLink', + 'transformFor', + 'keyForPolymorphicType', + '_normalizeArray', + 'deserialize', + 'defaultSerializer', + 'generateIdForRecord', + 'shouldReloadRecord', + 'shouldReloadAll', + 'shouldBackgroundReloadRecord', + 'shouldBackgroundReloadAll', + 'pluralize', + 'singularize', + 'enableCache', + 'purgedCache', + 'disableCache;', + 'plural', + 'singular', + 'uncountable', + 'irregular', + 'inflect', + 'columnNameToDesc', + 'columnsForType', + 'detect', + 'getFilters', + 'getRecordColor', + 'getRecordColumnValues', + 'getRecordFilterValues', + 'getRecordKeywords', + 'getRecords', + 'observerRecord', + 'pushPayload [OPTIONAL]', + 'serializeIntoHash [OPTIONAL]', + 'normalize [OPTIONAL]', + ], +}; diff --git a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts index 45526ace9c7..07337492994 100644 --- a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts @@ -13,7 +13,7 @@ import Adapter from '@ember-data/adapter'; import Serializer from '@ember-data/serializer'; import { resolve, all } from 'rsvp'; import { ExistingResourceObject } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import { ConfidentDict } from '@ember-data/store/-private/ts-interfaces/utils'; import { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; import { identifierCacheFor } from '@ember-data/store/-private'; import { set } from '@ember/object'; @@ -30,8 +30,8 @@ if (IDENTIFIERS) { let store; let calls; let secondaryCache: { - id: Dict; - username: Dict; + id: ConfidentDict; + username: ConfidentDict; }; class TestSerializer extends Serializer { normalizeResponse(_, __, payload) { @@ -232,7 +232,7 @@ if (IDENTIFIERS) { module('Secondary Cache using an attribute as an alternate id', function(hooks) { let store; let calls; - let secondaryCache: Dict; + let secondaryCache: ConfidentDict; class TestSerializer extends Serializer { normalizeResponse(_, __, payload) { return payload; diff --git a/packages/adapter/addon/-private/index.js b/packages/adapter/addon/-private/index.js index e244a107c58..4d1d18fb8df 100644 --- a/packages/adapter/addon/-private/index.js +++ b/packages/adapter/addon/-private/index.js @@ -7,3 +7,4 @@ export { determineBodyPromise } from './utils/determine-body-promise'; export { serializeQueryParams } from './utils/serialize-query-params'; export { default as fetch } from './utils/fetch'; export { default as BuildURLMixin } from './build-url-mixin'; +export { default as serializeIntoHash } from './utils/serialize-into-hash'; diff --git a/packages/adapter/addon/-private/utils/serialize-into-hash.js b/packages/adapter/addon/-private/utils/serialize-into-hash.js new file mode 100644 index 00000000000..daa20881f70 --- /dev/null +++ b/packages/adapter/addon/-private/utils/serialize-into-hash.js @@ -0,0 +1,11 @@ +export default function serializeIntoHash(store, modelClass, snapshot, options = { includeId: true }) { + const serializer = store.serializerFor(modelClass.modelName); + + if (typeof serializer.serializeIntoHash === 'function') { + const data = {}; + serializer.serializeIntoHash(data, modelClass, snapshot, options); + return data; + } + + return serializer.serialize(snapshot, options); +} diff --git a/packages/adapter/addon/json-api.js b/packages/adapter/addon/json-api.js index d86da31278a..a241d8b9efe 100644 --- a/packages/adapter/addon/json-api.js +++ b/packages/adapter/addon/json-api.js @@ -4,6 +4,7 @@ import { dasherize } from '@ember/string'; import RESTAdapter from './rest'; import { pluralize } from 'ember-inflector'; +import { serializeIntoHash } from './-private'; /** The `JSONAPIAdapter` is the default adapter used by Ember Data. It @@ -228,12 +229,8 @@ const JSONAPIAdapter = RESTAdapter.extend({ return pluralize(dasherized); }, - // TODO: Remove this once we have a better way to override HTTP verbs. updateRecord(store, type, snapshot) { - let data = {}; - let serializer = store.serializerFor(type.modelName); - - serializer.serializeIntoHash(data, type, snapshot, { includeId: true }); + const data = serializeIntoHash(store, type, snapshot); let url = this.buildURL(type.modelName, snapshot.id, snapshot, 'updateRecord'); diff --git a/packages/adapter/addon/rest.js b/packages/adapter/addon/rest.js index 0e2cb6bd0ce..a95a35e6f8f 100644 --- a/packages/adapter/addon/rest.js +++ b/packages/adapter/addon/rest.js @@ -23,6 +23,7 @@ import AdapterError, { } from '@ember-data/adapter/error'; import { warn } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; +import { serializeIntoHash } from './-private'; const Promise = EmberPromise; const hasJQuery = typeof jQuery !== 'undefined'; @@ -725,13 +726,11 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { @return {Promise} promise */ createRecord(store, type, snapshot) { - let data = {}; - let serializer = store.serializerFor(type.modelName); let url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); - serializer.serializeIntoHash(data, type, snapshot, { includeId: true }); + const data = serializeIntoHash(store, type, snapshot); - return this.ajax(url, 'POST', { data: data }); + return this.ajax(url, 'POST', { data }); }, /** @@ -751,15 +750,12 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { @return {Promise} promise */ updateRecord(store, type, snapshot) { - let data = {}; - let serializer = store.serializerFor(type.modelName); - - serializer.serializeIntoHash(data, type, snapshot); + const data = serializeIntoHash(store, type, snapshot, {}); let id = snapshot.id; let url = this.buildURL(type.modelName, id, snapshot, 'updateRecord'); - return this.ajax(url, 'PUT', { data: data }); + return this.ajax(url, 'PUT', { data }); }, /** diff --git a/packages/store/addon/-private/identifiers/cache.ts b/packages/store/addon/-private/identifiers/cache.ts index c16ca8283d2..49bc2aea3f0 100644 --- a/packages/store/addon/-private/identifiers/cache.ts +++ b/packages/store/addon/-private/identifiers/cache.ts @@ -1,6 +1,6 @@ import { DEBUG } from '@glimmer/env'; import { warn } from '@ember/debug'; -import { Dict } from '../ts-interfaces/utils'; +import { ConfidentDict } from '../ts-interfaces/utils'; import { ResourceIdentifierObject, ExistingResourceObject } from '../ts-interfaces/ember-data-json-api'; import { StableRecordIdentifier, @@ -30,8 +30,8 @@ interface KeyOptions { _allIdentifiers: StableRecordIdentifier[]; } -type IdentifierMap = Dict; -type TypeMap = Dict; +type IdentifierMap = ConfidentDict; +type TypeMap = ConfidentDict; export type MergeMethod = ( targetIdentifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index d2342badec1..b181bfa2187 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -80,6 +80,7 @@ import Reference from './references/reference'; import { Dict } from '../ts-interfaces/utils'; import constructResource from '../utils/construct-resource'; +import { errorsArrayToHash } from './errors-utils'; const emberRun = emberRunLoop.backburner; const { ENV } = Ember; @@ -103,7 +104,10 @@ const HAS_ADAPTER_PACKAGE = has('@ember-data/adapter'); function deprecateTestRegistration(factoryType: 'adapter', factoryName: '-json-api'): void; function deprecateTestRegistration(factoryType: 'serializer', factoryName: '-json-api' | '-rest' | '-default'): void; -function deprecateTestRegistration(factoryType: 'serializer' | 'adapter', factoryName: '-json-api' | '-rest' | '-default'): void { +function deprecateTestRegistration( + factoryType: 'serializer' | 'adapter', + factoryName: '-json-api' | '-rest' | '-default' +): void { deprecate( `You looked up the ${factoryType} "${factoryName}" but it was not found. Likely this means you are using a legacy ember-qunit moduleFor helper. Add "needs: ['${factoryType}:${factoryName}']", "integration: true", or refactor to modern syntax to resolve this deprecation.`, false, @@ -1269,8 +1273,8 @@ abstract class CoreStore extends Service { if (missingInternalModels.length) { warn( 'Ember Data expected to find records with the following ids in the adapter response but they were missing: [ "' + - missingInternalModels.map(r => r.id).join('", "') + - '" ]', + missingInternalModels.map(r => r.id).join('", "') + + '" ]', false, { id: 'ds.store.missing-records-from-adapter', @@ -1291,9 +1295,9 @@ abstract class CoreStore extends Service { if (pair) { pair.resolver.reject( error || - new Error( - `Expected: '${internalModel}' to be present in the adapter provided payload, but it was not found.` - ) + new Error( + `Expected: '${internalModel}' to be present in the adapter provided payload, but it was not found.` + ) ); } } @@ -1331,12 +1335,12 @@ abstract class CoreStore extends Service { } if (totalInGroup > 1) { - (function (groupedInternalModels) { + (function(groupedInternalModels) { _findMany(adapter, store, modelName, ids, groupedInternalModels, optionsMap) - .then(function (foundInternalModels) { + .then(function(foundInternalModels) { handleFoundRecords(foundInternalModels, groupedInternalModels); }) - .catch(function (error) { + .catch(function(error) { rejectInternalModels(groupedInternalModels, error); }); })(groupedInternalModels); @@ -3015,7 +3019,7 @@ abstract class CoreStore extends Service { return internalModelFactoryFor(this).lookup(resource); } - serializeRecord(record: Record, options?: Dict): unknown { + serializeRecord(record: Record, options?: Dict): unknown { if (CUSTOM_MODEL_CLASS) { let identifier = recordIdentifierFor(record); let internalModel = internalModelFactoryFor(this).peek(identifier); @@ -3026,7 +3030,7 @@ abstract class CoreStore extends Service { } } - saveRecord(record: Record, options?: Dict): RSVP.Promise { + saveRecord(record: Record, options?: Dict): RSVP.Promise { if (CUSTOM_MODEL_CLASS) { let identifier = recordIdentifierFor(record); let internalModel = internalModelFactoryFor(this).peek(identifier); @@ -3360,11 +3364,15 @@ abstract class CoreStore extends Service { let adapter = this.adapterFor(modelName); let serializerName = get(adapter, 'defaultSerializer'); - deprecate(`store.serializerFor("${modelName}") resolved the "${serializerName}" serializer via the deprecated \`adapter.defaultSerializer\` property.\n\n\tPreviously, if no application or type-specific serializer was specified, the store would attempt to lookup a serializer via the \`defaultSerializer\` property on the type's adapter. This behavior is deprecated in favor of explicitly defining a type-specific serializer or application serializer`, !serializerName, { - id: 'ember-data:default-serializer', - until: '4.0', - url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-serializers' - }); + deprecate( + `store.serializerFor("${modelName}") resolved the "${serializerName}" serializer via the deprecated \`adapter.defaultSerializer\` property.\n\n\tPreviously, if no application or type-specific serializer was specified, the store would attempt to lookup a serializer via the \`defaultSerializer\` property on the type's adapter. This behavior is deprecated in favor of explicitly defining a type-specific serializer or application serializer`, + !serializerName, + { + id: 'ember-data:default-serializer', + until: '4.0', + url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-serializers', + } + ); serializer = serializerName ? _serializerCache[serializerName] || owner.lookup(`serializer:${serializerName}`) @@ -3408,11 +3416,15 @@ abstract class CoreStore extends Service { serializer && deprecateTestRegistration('serializer', '-default'); } - deprecate(`store.serializerFor("${modelName}") resolved the "-default" serializer via the deprecated "-default" lookup fallback.\n\n\tPreviously, when no type-specific serializer, application serializer, or adapter.defaultSerializer had been defined by the app, the "-default" serializer would be used which defaulted to the \`JSONSerializer\`. This behavior is deprecated in favor of explicitly defining an application or type-specific serializer`, !serializer, { - id: 'ember-data:default-serializer', - until: '4.0', - url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-serializers' - }); + deprecate( + `store.serializerFor("${modelName}") resolved the "-default" serializer via the deprecated "-default" lookup fallback.\n\n\tPreviously, when no type-specific serializer, application serializer, or adapter.defaultSerializer had been defined by the app, the "-default" serializer would be used which defaulted to the \`JSONSerializer\`. This behavior is deprecated in favor of explicitly defining an application or type-specific serializer`, + !serializer, + { + id: 'ember-data:default-serializer', + until: '4.0', + url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-serializers', + } + ); assert( `No serializer was found for '${modelName}' and no 'application' serializer was found as a fallback`, @@ -3448,12 +3460,12 @@ abstract class CoreStore extends Service { if (shouldTrack) { throw new Error( 'Async Request leaks detected. Add a breakpoint here and set `store.generateStackTracesForTrackedRequests = true;`to inspect traces for leak origins:\n\t - ' + - tracked.map(o => o.label).join('\n\t - ') + tracked.map(o => o.label).join('\n\t - ') ); } else { warn( 'Async Request leaks detected. Add a breakpoint here and set `store.generateStackTracesForTrackedRequests = true;`to inspect traces for leak origins:\n\t - ' + - tracked.map(o => o.label).join('\n\t - '), + tracked.map(o => o.label).join('\n\t - '), false, { id: 'ds.async.leak.detected', @@ -3506,14 +3518,15 @@ abstract class CoreStore extends Service { defineProperty( CoreStore.prototype, 'defaultAdapter', - computed('adapter', function () { + computed('adapter', function() { deprecate( - `store.adapterFor(modelName) resolved the ("${this.adapter || '-json-api'}") adapter via the deprecated \`store.defaultAdapter\` property.\n\n\tPreviously, applications could define the store's \`adapter\` property which would be used by \`defaultAdapter\` and \`adapterFor\` as a fallback for when an adapter was not found by an exact name match. This behavior is deprecated in favor of explicitly defining an application or type-specific adapter.`, + `store.adapterFor(modelName) resolved the ("${this.adapter || + '-json-api'}") adapter via the deprecated \`store.defaultAdapter\` property.\n\n\tPreviously, applications could define the store's \`adapter\` property which would be used by \`defaultAdapter\` and \`adapterFor\` as a fallback for when an adapter was not found by an exact name match. This behavior is deprecated in favor of explicitly defining an application or type-specific adapter.`, false, { id: 'ember-data:default-adapter', until: '4.0', - url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-adapter' + url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_ember-data:default-adapter', } ); let adapter = this.adapter || '-json-api'; @@ -3580,9 +3593,16 @@ function _commit(adapter, store, operation, snapshot) { return internalModel; }, - function (error) { + function(error) { if (error instanceof InvalidError) { - let parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id); + let parsedErrors; + + if (typeof serializer.extractErrors === 'function') { + parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id); + } else { + parsedErrors = errorsArrayToHash(error.errors); + } + store.recordWasInvalid(internalModel, parsedErrors, error); } else { store.recordWasError(internalModel, error); diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index 415a5f1f3f4..bb6ceb1cdc5 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -8,7 +8,6 @@ import { normalizeResponseHelper } from './store/serializer-response'; import { InvalidError } from '@ember-data/adapter/error'; import coerceId from './coerce-id'; import { A } from '@ember/array'; - import { _findHasMany, _findBelongsTo, _findAll, _query, _queryRecord } from './store/finders'; import RequestCache from './request-cache'; import { CollectionResourceDocument, SingleResourceDocument } from '../ts-interfaces/ember-data-json-api'; @@ -18,6 +17,7 @@ import { symbol } from '../ts-interfaces/utils/symbol'; import Store from './ds-model-store'; import recordDataFor from './record-data-for'; import CoreStore from './core-store'; +import { errorsArrayToHash } from './errors-utils'; function payloadIsNotBlank(adapterPayload): boolean { if (Array.isArray(adapterPayload)) { @@ -135,7 +135,14 @@ export default class FetchManager { }, function(error) { if (error instanceof InvalidError) { - let parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id); + let parsedErrors = error.errors; + + if (typeof serializer.extractErrors === 'function') { + parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id); + } else { + parsedErrors = errorsArrayToHash(error.errors); + } + throw { error, parsedErrors }; } else { throw { error }; diff --git a/packages/store/addon/-private/system/identity-map.ts b/packages/store/addon/-private/system/identity-map.ts index f689f7e5847..01063192552 100644 --- a/packages/store/addon/-private/system/identity-map.ts +++ b/packages/store/addon/-private/system/identity-map.ts @@ -1,5 +1,5 @@ import InternalModelMap from './internal-model-map'; -import { Dict } from '../ts-interfaces/utils'; +import { ConfidentDict } from '../ts-interfaces/utils'; /** @module @ember-data/store @@ -13,7 +13,7 @@ import { Dict } from '../ts-interfaces/utils'; @private */ export default class IdentityMap { - private _map: Dict = Object.create(null); + private _map: ConfidentDict = Object.create(null); /** Retrieves the `InternalModelMap` for a given modelName, diff --git a/packages/store/addon/-private/system/internal-model-map.ts b/packages/store/addon/-private/system/internal-model-map.ts index 85c2a4e0006..3d0c48dfcf6 100644 --- a/packages/store/addon/-private/system/internal-model-map.ts +++ b/packages/store/addon/-private/system/internal-model-map.ts @@ -1,6 +1,6 @@ import { assert } from '@ember/debug'; import InternalModel from './model/internal-model'; -import { Dict } from '../ts-interfaces/utils'; +import { ConfidentDict } from '../ts-interfaces/utils'; /** @module @ember-data/store @@ -17,9 +17,9 @@ import { Dict } from '../ts-interfaces/utils'; @private */ export default class InternalModelMap { - private _idToModel: Dict = Object.create(null); + private _idToModel: ConfidentDict = Object.create(null); private _models: InternalModel[] = []; - private _metadata: Dict | null = null; + private _metadata: ConfidentDict | null = null; constructor(public modelName: string) {} @@ -104,7 +104,7 @@ export default class InternalModelMap { * @property metadata * @type Object */ - get metadata(): Dict { + get metadata(): ConfidentDict { return this._metadata || (this._metadata = Object.create(null)); } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 6c4a3c2a9af..7248644cbc0 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -23,7 +23,7 @@ import { default as recordDataFor, relationshipStateFor } from '../record-data-f import RecordData from '../../ts-interfaces/record-data'; import { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api'; import { Record } from '../../ts-interfaces/record'; -import { Dict } from '../../ts-interfaces/utils'; +import { ConfidentDict } from '../../ts-interfaces/utils'; import { IDENTIFIERS, RECORD_DATA_ERRORS, @@ -119,14 +119,14 @@ export default class InternalModel { __recordArrays: any; _references: any; _recordReference: any; - _manyArrayCache: Dict = Object.create(null); + _manyArrayCache: ConfidentDict = Object.create(null); // The previous ManyArrays for this relationship which will be destroyed when // we create a new ManyArray, but in the interim the retained version will be // updated if inverse internal models are unloaded. - _retainedManyArrayCache: Dict = Object.create(null); - _relationshipPromisesCache: Dict> = Object.create(null); - _relationshipProxyCache: Dict = Object.create(null); + _retainedManyArrayCache: ConfidentDict = Object.create(null); + _relationshipPromisesCache: ConfidentDict> = Object.create(null); + _relationshipProxyCache: ConfidentDict = Object.create(null); currentState: any; error: any; diff --git a/packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts b/packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts index d5c39f50e9e..7fadb3b0937 100644 --- a/packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts +++ b/packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts @@ -5,7 +5,7 @@ import { Dict } from './utils'; @module @ember-data/store */ -export type Meta = Dict; +export type Meta = Dict; /** * Serves as a reference to a `Resource` but does not contain @@ -74,15 +74,15 @@ export type ResourceIdentifierObject = ExistingResourceIdentifierObject | NewRes * Contains the data for an existing resource in JSON:API format */ export interface ExistingResourceObject extends ExistingResourceIdentifierObject { - meta?: Dict; - attributes?: Dict; + meta?: Dict; + attributes?: Dict; // these are lossy, need improved typing - relationships?: Dict; - links?: Dict; + relationships?: Dict; + links?: Dict; } interface Document { - meta?: Dict; + meta?: Dict; included?: ExistingResourceObject[]; } diff --git a/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts b/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts new file mode 100644 index 00000000000..f6819d9c2d7 --- /dev/null +++ b/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts @@ -0,0 +1,319 @@ +/** + ## Overview + + In order to properly manage and present your data, `EmberData` + needs to understand the structure of data it receives. + + `Serializers` convert data between the server's API format and + the format `EmberData` understands. + + Data received from an API response is `"normalized"` into + [JSON:API](https://jsonapi.org/) (the format used internally + by `EmberData`), while data sent to an API is `"serialized"` + into the format the API expects. + + ### Implementing a Serializer + + There are only two required serializer methods, one for + normalizing data from the server API format into `JSON:API`, and + another for serializing records via `Snapshot`s into the expected + server API format. + + To implement a serializer, export a class that conforms to the structure + described by the [MinimumSerializerInterface](MinimumSerializerInterface) + from the `app/serializers/` directory. An example is below. + + ```ts + import EmberObject from '@ember/object'; + + export default class ApplicationSerializer extends EmberObject { + normalizeResponse(store, schema, rawPayload) { + return rawPayload; + } + + serialize(snapshot, options) { + const serializedResource = { + id: snapshot.id(), + type: snapshot.modelName, + attributes: snapshot.attributes() + }; + + return serializedResource; + } + } + ``` + + + #### Serializer Resolution + + `store.serializerFor(name)` will lookup serializers defined in + `app/serializers` and return an instance. If no serializer is found, an + error will be thrown. + + `serializerFor` first attempts to find a serializer with an exact match on `name`, + then falls back to checking for the presence of a serializer named `application`. + + ```ts + store.serializerFor('author'); + + // lookup paths (in order) => + // app/serializers/author.js + // app/serializers/application.js + ``` + + Most requests in `ember-data` are made with respect to a particular `type` (or `modelName`) + (e.g., "get me the full collection of **books**" or "get me the **employee** whose id is 37"). We + refer to this as the *"primary"* resource `type`. + + Typically `serializerFor` will be used to find a serializer with a name matching that of the primary + resource `type` for the request, falling back to the `application` serializer for those types that + do not have a defined serializer. This is often described as a `per-model` or `per-type` strategy + for defining serializers. However, because APIs rarely format payloads per-type but rather + per-API-version, this may not be a desired strategy. + + It is recommended that applications define only a single `application` adapter and serializer + where possible. + + If you have multiple API formats and the per-type strategy is not viable, one strategy is to + write an `application` adapter and serializer that make use of `options` to specify the desired + format when making a request. + + ### Using a Serializer + + Any serializer in `app/serializers` can be looked up by `name` using `store.serializerFor(name)`. + + ### Default Serializers + + For applications whose APIs are *very close to* or *exactly* the `REST` or `JSON:API` + format the `@ember-data/serializer` package contains implementations these applications can + extend. It also contains a simple `JSONSerializer` for serializing to/from very basic JSON objects. + + Many applications will find writing their own serializer to be more performant and less + complex than extending these classes even when their API format is very close to that expected + by these serializers. + + It is recommended that apps write their own serializer to best suit the needs of their API and + application. + + @module @ember-data/serializer + @main @ember-data/serializer + @class MinimumSerializerInterface + @public +*/ + +import { Object as JSONObject } from 'json-typescript'; +import Store from '../system/core-store'; +import { JsonApiDocument, SingleResourceDocument } from './ember-data-json-api'; +import Snapshot from '../system/snapshot'; +import ShimModelClass from '../system/model/shim-model-class'; +import { Dict } from './utils'; + +type OptionsHash = Dict; + +interface Serializer { + /** + * This method is responsible for normalizing the value resolved from the promise returned + * by an Adapter request into the format expected by the `Store`. + * + * The output should be a [JSON:API Document](https://jsonapi.org/format/#document-structure) + * with the following additional restrictions: + * + * - `type` should be formatted in the `singular` `dasherized` `lowercase` form + * - `members` (the property names of attributes and relationships) should be formatted + * to match their definition in the corresponding `Model` definition. Typically this + * will be `camelCase`. + * - [`lid`](https://github.com/emberjs/rfcs/blob/master/text/0403-ember-data-identifiers.md) is + * a valid optional sibling to `id` and `type` in both [Resources](https://jsonapi.org/format/#document-resource-objects) + * and [Resource Identifier Objects](https://jsonapi.org/format/#document-resource-identifier-objects) + * + * @method normalizeResponse + * @public + * @param {Store} store - the store service that initiated the request being normalized + * @param {ShimModelClass} schema - An object with methods for accessing information about + * the type, attributes and relationships of the primary type associated with the request. + * @param {JSONObject} rawPayload - The raw JSON response data returned from an API request. + * This correlates to the value the promise returned by the adapter method that performed + * the request resolved to. + * @param {string|null} id - For a `findRecord` request, this is the `id` initially provided + * in the call to `store.findRecord`. Else this value is `null`. + * @param {'findRecord' | 'queryRecord' | 'findAll' | 'findBelongsTo' | 'findHasMany' | 'findMany' | 'query' | 'createRecord' | 'deleteRecord' | 'updateRecord'} requestType - The + * type of request the Adapter had been asked to perform. + * + * @returns {JsonApiDocument} - a document following the structure of a [JSON:API Document](https://jsonapi.org/format/#document-structure). + */ + normalizeResponse( + store: Store, + schema: ShimModelClass, + rawPayload: JSONObject, + id: string | null, + requestType: + | 'findRecord' + | 'queryRecord' + | 'findAll' + | 'findBelongsTo' + | 'findHasMany' + | 'findMany' + | 'query' + | 'createRecord' + | 'deleteRecord' + | 'updateRecord' + ): JsonApiDocument; + + /** + * This method is responsible for serializing an individual record + * via a [Snapshot](Snapshot) into the format expected by the API. + * + * This method is called by `snapshot.serialize()`. + * + * When using `Model`, this method is called by `record.serialize()`. + * + * When using `JSONAPIAdapter` or `RESTAdapter` this method is called + * by `updateRecord` and `createRecord` if `Serializer.serializeIntoHash` + * is not implemented. + * + * @method serialize + * @public + * @param {Snapshot} snapshot - A Snapshot for the record to serialize + * @param {object} [options] + */ + serialize(snapshot: Snapshot, options?: OptionsHash): JSONObject; + + /** + * This method is intended to normalize data into a [JSON:API Document](https://jsonapi.org/format/#document-structure) + * with a data member containing a single [Resource](https://jsonapi.org/format/#document-resource-objects). + * + * - `type` should be formatted in the `singular` `dasherized` `lowercase` form + * - `members` (the property names of attributes and relationships) should be formatted + * to match their definition in the corresponding `Model` definition. Typically this + * will be `camelCase`. + * - [`lid`](https://github.com/emberjs/rfcs/blob/master/text/0403-ember-data-identifiers.md) is + * a valid optional sibling to `id` and `type` in both [Resources](https://jsonapi.org/format/#document-resource-objects) + * and [Resource Identifier Objects](https://jsonapi.org/format/#document-resource-identifier-objects) + * + * This method is called by the `Store` when `store.normalize(modelName, payload)` is + * called. It is recommended to use `store.serializerFor(modelName).normalizeResponse` + * over `store.normalize`. + * + * This method may be called when also using the `RESTSerializer` + * when `serializer.pushPayload` is called by `store.pushPayload`. + * It is recommended to use `store.push` over `store.pushPayload` after normalizing + * the payload directly. + * + * Example: + * ```js + * function pushPayload(store, modelName, rawPayload) { + * const ModelClass = store.modelFor(modelName); + * const serializer = store.serializerFor(modelName); + * const jsonApiPayload = serializer.normalizeResponse(store, ModelClass, rawPayload, null, 'query'); + * + * return store.push(jsonApiPayload); + * } + * ``` + * + * This method may be called when also using the `JSONAPISerializer` + * when normalizing included records. If mixing serializer usage in this way + * we recommend implementing this method, but caution that it may lead + * to unexpected mixing of formats. + * + * This method may also be called when normalizing embedded relationships when + * using the `EmbeddedRecordsMixin`. If using this mixin in a serializer in + * your application we recommend implementing this method, but caution that + * it may lead to unexpected mixing of formats. + * + * @method normalize [OPTIONAL] + * @public + * @optional + * @param {ShimModelClass} schema - An object with methods for accessing information about + * the type, attributes and relationships of the primary type associated with the request. + * @param {JSONObject} rawPayload - Some raw JSON data to be normalized into a [JSON:API Resource](https://jsonapi.org/format/#document-resource-objects). + * @param {string} [prop] - When called by the `EmbeddedRecordsMixin` this param will be the + * property at which the object provided as rawPayload was found. + * @returns {SingleResourceDocument} - A [JSON:API Document](https://jsonapi.org/format/#document-structure) + * containing a single [JSON:API Resource](https://jsonapi.org/format/#document-resource-objects) + * as its primary data. + */ + normalize?(schema: ShimModelClass, rawPayload: JSONObject, prop?: string): SingleResourceDocument; + + /** + * When using `JSONAPIAdapter` or `RESTAdapter` this method is called + * by `adapter.updateRecord` and `adapter.createRecord` if `Serializer.serializeIntoHash` + * is not implemented. + * + * You can use this method to customize the root keys serialized into the payload. + * The hash property should be modified by reference. + * + * For instance, your API may expect resources to be keyed by underscored type in the payload: + * + * ```js + * { + * _user: { + * type: 'user', + * id: '1' + * } + * } + * ``` + * + * Which when using these adapters can be achieved by implementing this method similar + * to the following: + * + * ```js + * serializeIntoHash(hash, ModelClass, snapshot, options) { + * hash[`_${snapshot.modelName}`] = this.serialize(snapshot, options).data; + * } + * ``` + * + * @method serializeIntoHash [OPTIONAL] + * @public + * @optional + * @param hash - a top most object of the request payload onto + * which to append the serialized record + * @param {ShimModelClass} schema - An object with methods for accessing information about + * the type, attributes and relationships of the primary type associated with the request. + * @param {Snapshot} snapshot - A Snapshot for the record to serialize + * @param [options] + * @returns {void} + */ + serializeIntoHash?(hash: object, schema: ShimModelClass, snapshot: Snapshot, options?: OptionsHash): void; + + /** + * This method allows for normalization of data when `store.pushPayload` is called + * and should be implemented if you want to use that method. + * + * The output should be a [JSON:API Document](https://jsonapi.org/format/#document-structure) + * with the following additional restrictions: + * + * - `type` should be formatted in the `singular` `dasherized` `lowercase` form + * - `members` (the property names of attributes and relationships) should be formatted + * to match their definition in the corresponding `Model` definition. Typically this + * will be `camelCase`. + * - [`lid`](https://github.com/emberjs/rfcs/blob/master/text/0403-ember-data-identifiers.md) is + * a valid optional sibling to `id` and `type` in both [Resources](https://jsonapi.org/format/#document-resource-objects) + * and [Resource Identifier Objects](https://jsonapi.org/format/#document-resource-identifier-objects) + * + * If you need better control over normalization or want access to the records being added or updated + * in the store, we recommended using `store.push` over `store.pushPayload` after normalizing + * the payload directly. This can even take advantage of an existing serializer for the format + * the data is in, for example: + * + * ```js + * function pushPayload(store, modelName, rawPayload) { + * const ModelClass = store.modelFor(modelName); + * const serializer = store.serializerFor(modelName); + * const jsonApiPayload = serializer.normalizeResponse(store, ModelClass, rawPayload, null, 'query'); + * + * return store.push(jsonApiPayload); + * } + * ``` + * + * @method pushPayload [OPTIONAL] + * @public + * @optional + * @param {Store} store - the store service that initiated the request being normalized + * @param {JSONObject} rawPayload - The raw JSON response data returned from an API request. + * This JSON should be in the API format expected by the serializer. + * @returns {JsonApiDocument} - a document following the structure of a [JSON:API Document](https://jsonapi.org/format/#document-structure) + */ + pushPayload?(store: Store, rawPayload: JSONObject): JsonApiDocument; +} + +export default Serializer; diff --git a/packages/store/addon/-private/ts-interfaces/utils.ts b/packages/store/addon/-private/ts-interfaces/utils.ts index 3c6af61e96a..43d92465412 100644 --- a/packages/store/addon/-private/ts-interfaces/utils.ts +++ b/packages/store/addon/-private/ts-interfaces/utils.ts @@ -2,4 +2,5 @@ @module @ember-data/store */ -export type Dict = { [KK in K]: V }; +export type ConfidentDict = { [key: string]: V }; +export type Dict = { [key: string]: V | undefined };