diff --git a/packages/datastore/__tests__/DataStore.ts b/packages/datastore/__tests__/DataStore.ts index 95123de8a4c..03e713ee070 100644 --- a/packages/datastore/__tests__/DataStore.ts +++ b/packages/datastore/__tests__/DataStore.ts @@ -389,13 +389,12 @@ describe('DataStore tests', () => { const result = await DataStore.save(model); const [settingsSave, modelCall] = save.mock.calls; - const [_model, _condition, _mutator, patches] = modelCall; + const [_model, _condition, _mutator] = modelCall; expect(result).toMatchObject(model); - expect(patches).toBeUndefined(); }); - test('Save returns the updated model and patches', async () => { + test('Save returns the updated model', async () => { let model: Model; const save = jest.fn(() => [model]); const query = jest.fn(() => [model]); @@ -438,17 +437,12 @@ describe('DataStore tests', () => { const result = await DataStore.save(model); const [settingsSave, modelSave, modelUpdate] = save.mock.calls; - const [_model, _condition, _mutator, patches] = modelUpdate; - - const expectedPatches = [ - { op: 'replace', path: ['field1'], value: 'edited' }, - ]; + const [_model, _condition, _mutator] = modelUpdate; expect(result).toMatchObject(model); - expect(patches).toMatchObject(expectedPatches); }); - test('Save returns the updated model and patches - list field', async () => { + test('Save returns the updated model - list field', async () => { let model: Model; const save = jest.fn(() => [model]); const query = jest.fn(() => [model]); @@ -505,27 +499,8 @@ describe('DataStore tests', () => { save.mock.calls ); - const [_model, _condition, _mutator, patches] = modelUpdate; - const [_model2, _condition2, _mutator2, patches2] = modelUpdate2; - - const expectedPatches = [ - { - op: 'replace', - path: ['emails'], - value: ['john@doe.com', 'jane@doe.com', 'joe@doe.com'], - }, - ]; - - const expectedPatches2 = [ - { - op: 'add', - path: ['emails', 3], - value: 'joe@doe.com', - }, - ]; - - expect(patches).toMatchObject(expectedPatches); - expect(patches2).toMatchObject(expectedPatches2); + const [_model, _condition, _mutator] = modelUpdate; + const [_model2, _condition2, _mutator2] = modelUpdate2; }); test('Instantiation validations', async () => { diff --git a/packages/datastore/__tests__/helpers.ts b/packages/datastore/__tests__/helpers.ts index a9e81d12d9d..2162ed6df4d 100644 --- a/packages/datastore/__tests__/helpers.ts +++ b/packages/datastore/__tests__/helpers.ts @@ -16,7 +16,6 @@ export declare class Model { mutator: (draft: MutableModel) => void | Model ): Model; } - export declare class Metadata { readonly author: string; readonly tags?: string[]; @@ -27,6 +26,17 @@ export declare class Metadata { constructor(init: Metadata); } +export declare class Post { + public readonly id: string; + public readonly title: string; +} + +export declare class Comment { + public readonly id: string; + public readonly content: string; + public readonly post: Post; +} + export function testSchema(): Schema { return { enums: {}, @@ -88,6 +98,94 @@ export function testSchema(): Schema { }, }, }, + Post: { + name: 'Post', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + title: { + name: 'title', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + comments: { + name: 'comments', + isArray: true, + type: { + model: 'Comment', + }, + isRequired: true, + attributes: [], + isArrayNullable: true, + association: { + connectionType: 'HAS_MANY', + associatedWith: 'postId', + }, + }, + }, + syncable: true, + pluralName: 'Posts', + attributes: [ + { + type: 'model', + properties: {}, + }, + ], + }, + Comment: { + name: 'Comment', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + content: { + name: 'content', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + post: { + name: 'post', + isArray: false, + type: { + model: 'Post', + }, + isRequired: false, + attributes: [], + association: { + connectionType: 'BELONGS_TO', + targetName: 'postId', + }, + }, + }, + syncable: true, + pluralName: 'Comments', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'key', + properties: { + name: 'byPost', + fields: ['postId'], + }, + }, + ], + }, LocalModel: { name: 'LocalModel', pluralName: 'LocalModels', diff --git a/packages/datastore/__tests__/storage.test.ts b/packages/datastore/__tests__/storage.test.ts index 8402eaa7486..63f5fcaa2a2 100644 --- a/packages/datastore/__tests__/storage.test.ts +++ b/packages/datastore/__tests__/storage.test.ts @@ -4,211 +4,328 @@ import { initSchema as initSchemaType, } from '../src/datastore/datastore'; import { PersistentModelConstructor } from '../src/types'; -import { Model, testSchema } from './helpers'; +import { Model, Post, Comment, testSchema } from './helpers'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; describe('Storage tests', () => { describe('Update', () => { - let zenNext; + describe('Only include changed fields', () => { + let zenNext; - beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); - zenNext = jest.fn(); + zenNext = jest.fn(); - jest.doMock('zen-push', () => { - class zenPush { - constructor() {} - next = zenNext; - } + jest.doMock('zen-push', () => { + class zenPush { + constructor() {} + next = zenNext; + } - return zenPush; + return zenPush; + }); + + ({ initSchema, DataStore } = require('../src/datastore/datastore')); }); - ({ initSchema, DataStore } = require('../src/datastore/datastore')); - }); + test('scalar', async () => { + const classes = initSchema(testSchema()); - test('Only include changed fields - scalar', async () => { - const classes = initSchema(testSchema()); + const { Model } = classes as { + Model: PersistentModelConstructor; + }; - const { Model } = classes as { - Model: PersistentModelConstructor; - }; + const dateCreated = new Date().toISOString(); - const dateCreated = new Date().toISOString(); + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated, + }) + ); - const model = await DataStore.save( - new Model({ - field1: 'Some value', - dateCreated, - }) - ); + await DataStore.save( + Model.copyOf(model, draft => { + draft.field1 = 'edited'; + }) + ); - await DataStore.save( - Model.copyOf(model, draft => { - draft.field1 = 'edited'; - }) - ); + const [_settingsSave, [modelSave], [modelUpdate]] = zenNext.mock.calls; - const [_settingsSave, [modelSave], [modelUpdate]] = zenNext.mock.calls; + // Save should include + expect(modelSave.element.dateCreated).toEqual(dateCreated); - // Save should include - expect(modelSave.element.dateCreated).toEqual(dateCreated); + // Update mutation should only include updated fields + // => dateCreated should be undefined + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toEqual('edited'); + }); - // Update mutation should only include updated fields - // => dateCreated should be undefined - expect(modelUpdate.element.dateCreated).toBeUndefined(); - expect(modelUpdate.element.field1).toEqual('edited'); - }); + test('list (destructured)', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + emails: ['john@doe.com', 'jane@doe.com'], + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.emails = [...draft.emails, 'joe@doe.com']; + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + const expectedValueEmails = [ + 'john@doe.com', + 'jane@doe.com', + 'joe@doe.com', + ]; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toBeUndefined(); + expect(modelUpdate.element.emails).toMatchObject(expectedValueEmails); + }); - test('Only include changed fields - list (destructured)', async () => { - const classes = initSchema(testSchema()); - - const { Model } = classes as { - Model: PersistentModelConstructor; - }; - - const model = await DataStore.save( - new Model({ - field1: 'Some value', - dateCreated: new Date().toISOString(), - emails: ['john@doe.com', 'jane@doe.com'], - }) - ); - - await DataStore.save( - Model.copyOf(model, draft => { - draft.emails = [...draft.emails, 'joe@doe.com']; - }) - ); - - const [[modelSave], [modelUpdate]] = zenNext.mock.calls; - - const expectedValueEmails = [ - 'john@doe.com', - 'jane@doe.com', - 'joe@doe.com', - ]; - - expect(modelUpdate.element.dateCreated).toBeUndefined(); - expect(modelUpdate.element.field1).toBeUndefined(); - expect(modelUpdate.element.emails).toMatchObject(expectedValueEmails); - }); + test('list (push)', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + emails: ['john@doe.com', 'jane@doe.com'], + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.emails.push('joe@doe.com'); + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + const expectedValueEmails = [ + 'john@doe.com', + 'jane@doe.com', + 'joe@doe.com', + ]; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toBeUndefined(); + expect(modelUpdate.element.emails).toMatchObject(expectedValueEmails); + }); - test('Only include changed fields - list (push)', async () => { - const classes = initSchema(testSchema()); - - const { Model } = classes as { - Model: PersistentModelConstructor; - }; - - const model = await DataStore.save( - new Model({ - field1: 'Some value', - dateCreated: new Date().toISOString(), - emails: ['john@doe.com', 'jane@doe.com'], - }) - ); - - await DataStore.save( - Model.copyOf(model, draft => { - draft.emails.push('joe@doe.com'); - }) - ); - - const [[modelSave], [modelUpdate]] = zenNext.mock.calls; - - const expectedValueEmails = [ - 'john@doe.com', - 'jane@doe.com', - 'joe@doe.com', - ]; - - expect(modelUpdate.element.dateCreated).toBeUndefined(); - expect(modelUpdate.element.field1).toBeUndefined(); - expect(modelUpdate.element.emails).toMatchObject(expectedValueEmails); - }); + test('list unchanged', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + emails: ['john@doe.com', 'jane@doe.com'], + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.field1 = 'Updated value'; + // same as above. should not be included in mutation input + draft.emails = ['john@doe.com', 'jane@doe.com']; + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toEqual('Updated value'); + expect(modelUpdate.element.emails).toBeUndefined(); + }); - test('Only include changed fields - custom type (destructured)', async () => { - const classes = initSchema(testSchema()); - - const { Model } = classes as { - Model: PersistentModelConstructor; - }; - - const model = await DataStore.save( - new Model({ - field1: 'Some value', - dateCreated: new Date().toISOString(), - metadata: { - author: 'some author', - rewards: [], - penNames: [], - }, - }) - ); - - await DataStore.save( - Model.copyOf(model, draft => { - draft.metadata = { - ...draft.metadata, - penNames: ['bob'], - }; - }) - ); - - const [[modelSave], [modelUpdate]] = zenNext.mock.calls; - - const expectedValueMetadata = { - author: 'some author', - rewards: [], - penNames: ['bob'], - }; - - expect(modelUpdate.element.dateCreated).toBeUndefined(); - expect(modelUpdate.element.field1).toBeUndefined(); - expect(modelUpdate.element.metadata).toMatchObject(expectedValueMetadata); - }); + test('custom type (destructured)', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + metadata: { + author: 'some author', + rewards: [], + penNames: [], + }, + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.metadata = { + ...draft.metadata, + penNames: ['bob'], + }; + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + const expectedValueMetadata = { + author: 'some author', + rewards: [], + penNames: ['bob'], + }; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toBeUndefined(); + expect(modelUpdate.element.metadata).toMatchObject( + expectedValueMetadata + ); + }); + + test('custom type (accessor)', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + metadata: { + author: 'some author', + rewards: [], + penNames: [], + }, + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.metadata.penNames = ['bob']; + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + const expectedValueMetadata = { + author: 'some author', + rewards: [], + penNames: ['bob'], + }; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toBeUndefined(); + expect(modelUpdate.element.metadata).toMatchObject( + expectedValueMetadata + ); + }); + + test('custom type unchanged', async () => { + const classes = initSchema(testSchema()); + + const { Model } = classes as { + Model: PersistentModelConstructor; + }; + + const model = await DataStore.save( + new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + metadata: { + author: 'some author', + rewards: [], + penNames: [], + }, + }) + ); + + await DataStore.save( + Model.copyOf(model, draft => { + draft.field1 = 'Updated value'; + draft.metadata = { + author: 'some author', + rewards: [], + penNames: [], + }; + }) + ); + + const [[modelSave], [modelUpdate]] = zenNext.mock.calls; + + expect(modelUpdate.element.dateCreated).toBeUndefined(); + expect(modelUpdate.element.field1).toEqual('Updated value'); + expect(modelUpdate.element.metadata).toBeUndefined(); + }); - test('Only include changed fields - custom type (accessor)', async () => { - const classes = initSchema(testSchema()); - - const { Model } = classes as { - Model: PersistentModelConstructor; - }; - - const model = await DataStore.save( - new Model({ - field1: 'Some value', - dateCreated: new Date().toISOString(), - metadata: { - author: 'some author', - rewards: [], - penNames: [], - }, - }) - ); - - await DataStore.save( - Model.copyOf(model, draft => { - draft.metadata.penNames = ['bob']; - }) - ); - - const [[modelSave], [modelUpdate]] = zenNext.mock.calls; - - const expectedValueMetadata = { - author: 'some author', - rewards: [], - penNames: ['bob'], - }; - - expect(modelUpdate.element.dateCreated).toBeUndefined(); - expect(modelUpdate.element.field1).toBeUndefined(); - expect(modelUpdate.element.metadata).toMatchObject(expectedValueMetadata); + test('relation', async () => { + const classes = initSchema(testSchema()); + + const { Post, Comment } = classes as { + Post: PersistentModelConstructor; + Comment: PersistentModelConstructor; + }; + + const post = await DataStore.save( + new Post({ + title: 'New Post', + }) + ); + + const comment = await DataStore.save( + new Comment({ + content: 'Hello world', + post, + }) + ); + + const anotherPost = await DataStore.save( + new Post({ + title: 'Another Post', + }) + ); + + await DataStore.save( + Comment.copyOf(comment, updated => { + updated.post = anotherPost; + }) + ); + + const [ + [_post1Save], + [commentSave], + [_post2Save], + [commentUpdate], + ] = zenNext.mock.calls; + + expect(commentSave.element.postId).toEqual(post.id); + expect(commentUpdate.element.postId).toEqual(anotherPost.id); + }); }); }); }); diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 6fe1079483f..52863320063 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -1,12 +1,5 @@ import { Amplify, ConsoleLogger as Logger, Hub, JS } from '@aws-amplify/core'; -import { - Draft, - immerable, - produce, - setAutoFreeze, - enablePatches, - Patch, -} from 'immer'; +import { Draft, immerable, produce, setAutoFreeze } from 'immer'; import { v4 as uuid4 } from 'uuid'; import Observable, { ZenObservable } from 'zen-observable-ts'; import { @@ -61,7 +54,6 @@ import { } from '../util'; setAutoFreeze(true); -enablePatches(); const logger = new Logger('DataStore'); @@ -86,7 +78,6 @@ const modelNamespaceMap = new WeakMap< PersistentModelConstructor, string >(); -const modelPatchesMap = new WeakMap(); const getModelDefinition = ( modelConstructor: PersistentModelConstructor @@ -392,23 +383,14 @@ const createModelClass = ( throw new Error(msg); } - let patches; - const model = produce( - source, - draft => { - fn(>draft); - draft.id = source.id; - const modelValidator = validateModelFields(modelDefinition); - Object.entries(draft).forEach(([k, v]) => { - modelValidator(k, v); - }); - }, - p => (patches = p) - ); - - patches.length && modelPatchesMap.set(model, patches); - - return model; + return produce(source, draft => { + fn(>draft); + draft.id = source.id; + const modelValidator = validateModelFields(modelDefinition); + Object.entries(draft).forEach(([k, v]) => { + modelValidator(k, v); + }); + }); } // "private" method (that's hidden via `Setting`) for `withSSRContext` to use @@ -782,10 +764,6 @@ class DataStore { ): Promise => { await this.start(); - // Immer patches for constructing a correct update mutation input - // Allows us to only include changed fields for updates - const patches = modelPatchesMap.get(model); - const modelConstructor: PersistentModelConstructor = model ? >model.constructor : undefined; @@ -805,7 +783,7 @@ class DataStore { ); const [savedModel] = await this.storage.runExclusive(async s => { - await s.save(model, producedCondition, undefined, patches); + await s.save(model, producedCondition); return s.query( modelConstructor, diff --git a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts index 72bf106aa65..c7dddaf9de4 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts @@ -97,7 +97,7 @@ export class AsyncStorageAdapter implements Adapter { async save( model: T, condition?: ModelPredicate - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { + ): Promise<[T, OpType.INSERT | OpType.UPDATE, T?][]> { const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor; const storeName = this.getStorenameForModel(modelConstructor); @@ -133,25 +133,21 @@ export class AsyncStorageAdapter implements Adapter { } } - const result: [T, OpType.INSERT | OpType.UPDATE][] = []; + const result: [T, OpType.INSERT | OpType.UPDATE, T?][] = []; for await (const resItem of connectionStoreNames) { const { storeName, item, instance } = resItem; - const { id } = item; - const opType: OpType = (await this.db.get(id, storeName)) - ? OpType.UPDATE - : OpType.INSERT; + const fromDB = await this.db.get(id, storeName); + const opType: OpType = fromDB ? OpType.UPDATE : OpType.INSERT; - if (id === model.id) { + if (id === model.id || opType === OpType.INSERT) { await this.db.save(item, storeName); - result.push([instance, opType]); - } else { - if (opType === OpType.INSERT) { - await this.db.save(item, storeName); - + if (opType === OpType.UPDATE) { + result.push([instance, opType, fromDB]); + } else { result.push([instance, opType]); } } diff --git a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts index d250c108f5d..0e17040eba3 100644 --- a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts +++ b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts @@ -206,7 +206,7 @@ class IndexedDBAdapter implements Adapter { async save( model: T, condition?: ModelPredicate - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { + ): Promise<[T, OpType.INSERT | OpType.UPDATE, T?][]> { await this.checkPrivate(); const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor; @@ -250,31 +250,25 @@ class IndexedDBAdapter implements Adapter { } } - const result: [T, OpType.INSERT | OpType.UPDATE][] = []; + const result: [T, OpType.INSERT | OpType.UPDATE, T?][] = []; for await (const resItem of connectionStoreNames) { const { storeName, item, instance } = resItem; const store = tx.objectStore(storeName); - const { id } = item; + const fromDB = await this._get(store, id); const opType: OpType = - (await this._get(store, id)) === undefined - ? OpType.INSERT - : OpType.UPDATE; + fromDB === undefined ? OpType.INSERT : OpType.UPDATE; - // It is me - if (id === model.id) { + // Even if the parent is an INSERT, the child might not be, so we need to get its key + if (id === model.id || opType === OpType.INSERT) { const key = await store.index('byId').getKey(item.id); await store.put(item, key); - result.push([instance, opType]); - } else { - if (opType === OpType.INSERT) { - // Even if the parent is an INSERT, the child might not be, so we need to get its key - const key = await store.index('byId').getKey(item.id); - await store.put(item, key); - + if (opType === OpType.UPDATE) { + result.push([instance, opType, fromDB]); + } else { result.push([instance, opType]); } } diff --git a/packages/datastore/src/storage/adapter/index.ts b/packages/datastore/src/storage/adapter/index.ts index 9c7b54feb2f..4e7e638ae57 100644 --- a/packages/datastore/src/storage/adapter/index.ts +++ b/packages/datastore/src/storage/adapter/index.ts @@ -14,7 +14,7 @@ export interface Adapter extends SystemComponent { save( model: T, condition?: ModelPredicate - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]>; + ): Promise<[T, OpType.INSERT | OpType.UPDATE, T?][]>; delete: ( modelOrModelConstructor: T | PersistentModelConstructor, condition?: ModelPredicate diff --git a/packages/datastore/src/storage/storage.ts b/packages/datastore/src/storage/storage.ts index aa818c332e8..3a04e8751bb 100644 --- a/packages/datastore/src/storage/storage.ts +++ b/packages/datastore/src/storage/storage.ts @@ -18,7 +18,12 @@ import { SchemaNamespace, SubscriptionMessage, } from '../types'; -import { isModelConstructor, STORAGE, validatePredicate } from '../util'; +import { + isModelConstructor, + STORAGE, + validatePredicate, + getUpdateMutationInput, +} from '../util'; import { Adapter } from './adapter'; import getDefaultAdapter from './adapter/getDefaultAdapter'; @@ -99,45 +104,26 @@ class StorageClass implements StorageFacade { async save( model: T, condition?: ModelPredicate, - mutator?: Symbol, - patches?: Patch[] - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { + mutator?: Symbol + ): Promise<[T, OpType.INSERT | OpType.UPDATE, T?][]> { await this.init(); const result = await this.adapter.save(model, condition); result.forEach(r => { - const [originalElement, opType] = r; + const [savedElement, opType, fromDB] = r; let updatedElement; - if (opType === OpType.UPDATE && patches && patches.length) { - updatedElement = {}; - // extract array of updated fields from patches - const updatedFields = patches.map(patch => patch.path && patch.path[0]); - - // set original values for these fields - updatedFields.forEach(field => { - updatedElement[field] = originalElement[field]; - }); - - const { id, _version, _lastChangedAt, _deleted } = originalElement; - + if (opType === OpType.UPDATE && fromDB) { // For update mutations we only want to send fields with changes // and the required internal fields - updatedElement = { - ...updatedElement, - id, - _version, - _lastChangedAt, - _deleted, - }; + updatedElement = getUpdateMutationInput(fromDB, savedElement); } - const element = updatedElement || originalElement; + const element = updatedElement || savedElement; - const modelConstructor = (Object.getPrototypeOf( - originalElement - ) as Object).constructor as PersistentModelConstructor; + const modelConstructor = (Object.getPrototypeOf(savedElement) as Object) + .constructor as PersistentModelConstructor; this.pushStream.next({ model: modelConstructor, @@ -328,11 +314,10 @@ class ExclusiveStorage implements StorageFacade { async save( model: T, condition?: ModelPredicate, - mutator?: Symbol, - patches?: Patch[] - ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { - return this.runExclusive<[T, OpType.INSERT | OpType.UPDATE][]>(storage => - storage.save(model, condition, mutator, patches) + mutator?: Symbol + ): Promise<[T, OpType.INSERT | OpType.UPDATE, T?][]> { + return this.runExclusive<[T, OpType.INSERT | OpType.UPDATE, T?][]>( + storage => storage.save(model, condition, mutator) ); } diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index 81407316463..30cfbd71bb4 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -434,6 +434,34 @@ export function sortCompareFunction( }; } +export function getUpdateMutationInput( + original: T, + updated: T +): { [key: string]: any } { + const mutationInput: { [key: string]: any } = { + id: original.id, + _version: original._version, + _lastChangedAt: original._lastChangedAt, + _deleted: original._deleted, + }; + + for (const field in original) { + let originalValue: any = original[field]; + let updatedValue: any = updated[field]; + + if (typeof originalValue === 'object') { + originalValue = JSON.stringify(originalValue); + updatedValue = JSON.stringify(updatedValue); + } + + if (originalValue !== updatedValue) { + mutationInput[field] = updated[field]; + } + } + + return mutationInput; +} + export const isAWSDate = (val: string): boolean => { return !!/^\d{4}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2}($|:\d{2}))?$/.exec(val); };