UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

204 lines (201 loc) • 9.4 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.MutationEventOutbox = void 0; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 const predicates_1 = require("../predicates"); const types_1 = require("../types"); const util_1 = require("../util"); const utils_1 = require("./utils"); // TODO: Persist deleted ids // https://github.com/aws-amplify/amplify-js/blob/datastore-docs/packages/datastore/docs/sync-engine.md#outbox class MutationEventOutbox { constructor(schema, _MutationEvent, modelInstanceCreator, ownSymbol) { this.schema = schema; this._MutationEvent = _MutationEvent; this.modelInstanceCreator = modelInstanceCreator; this.ownSymbol = ownSymbol; } async enqueue(storage, mutationEvent) { await storage.runExclusive(async (s) => { const mutationEventModelDefinition = this.schema.namespaces[util_1.SYNC].models.MutationEvent; // `id` is the key for the record in the mutationEvent; // `modelId` is the key for the actual record that was mutated const predicate = predicates_1.ModelPredicateCreator.createFromAST(mutationEventModelDefinition, { and: [ { modelId: { eq: mutationEvent.modelId } }, { id: { ne: this.inProgressMutationEventId } }, ], }); // Check if there are any other records with same id const [first] = await s.query(this._MutationEvent, predicate); // No other record with same modelId, so enqueue if (first === undefined) { await s.save(mutationEvent, undefined, this.ownSymbol); return; } // There was an enqueued mutation for the modelId, so continue const { operation: incomingMutationType } = mutationEvent; if (first.operation === utils_1.TransformerMutationType.CREATE) { if (incomingMutationType === utils_1.TransformerMutationType.DELETE) { await s.delete(this._MutationEvent, predicate); } else { // first gets updated with the incoming mutation's data, condition intentionally skipped // we need to merge the fields for a create and update mutation to prevent // data loss, since update mutations only include changed fields const merged = this.mergeUserFields(first, mutationEvent); await s.save(this._MutationEvent.copyOf(first, draft => { draft.data = merged.data; }), undefined, this.ownSymbol); } } else { const { condition: incomingConditionJSON } = mutationEvent; const incomingCondition = JSON.parse(incomingConditionJSON); let merged; // If no condition if (Object.keys(incomingCondition).length === 0) { merged = this.mergeUserFields(first, mutationEvent); // delete all for model await s.delete(this._MutationEvent, predicate); } merged = merged || mutationEvent; // Enqueue new one await s.save(merged, undefined, this.ownSymbol); } }); } async dequeue(storage, record, recordOp) { const head = await this.peek(storage); if (record) { await this.syncOutboxVersionsOnDequeue(storage, record, head, recordOp); } if (head) { await storage.delete(head); } this.inProgressMutationEventId = undefined; return head; } /** * Doing a peek() implies that the mutation goes "inProgress" * * @param storage */ async peek(storage) { const head = await storage.queryOne(this._MutationEvent, types_1.QueryOne.FIRST); this.inProgressMutationEventId = head ? head.id : undefined; return head; } async getForModel(storage, model, userModelDefinition) { const mutationEventModelDefinition = this.schema.namespaces[util_1.SYNC].models.MutationEvent; const modelId = (0, utils_1.getIdentifierValue)(userModelDefinition, model); const mutationEvents = await storage.query(this._MutationEvent, predicates_1.ModelPredicateCreator.createFromAST(mutationEventModelDefinition, { and: { modelId: { eq: modelId } }, })); return mutationEvents; } async getModelIds(storage) { const mutationEvents = await storage.query(this._MutationEvent); const result = new Set(); mutationEvents.forEach(({ modelId }) => result.add(modelId)); return result; } // applies _version from the AppSync mutation response to other items // in the mutation queue with the same id // see https://github.com/aws-amplify/amplify-js/pull/7354 for more details async syncOutboxVersionsOnDequeue(storage, record, head, recordOp) { if (head?.operation !== recordOp) { return; } const { _version, _lastChangedAt, _deleted, ..._incomingData } = record; const incomingData = this.removeTimestampFields(head.model, _incomingData); const data = JSON.parse(head.data); if (!data) { return; } const { _version: __version, _lastChangedAt: __lastChangedAt, _deleted: __deleted, ..._outgoingData } = data; const outgoingData = this.removeTimestampFields(head.model, _outgoingData); // Don't sync the version when the data in the response does not match the data // in the request, i.e., when there's a handled conflict // // NOTE: `incomingData` contains all the fields in the record received from AppSync // and `outgoingData` only contains updated fields sent to AppSync // If all send data isn't matched in the returned data then the update was rejected // by AppSync and we should not update the version on other outbox entries for this // object if (!(0, util_1.directedValueEquality)(outgoingData, incomingData, true)) { return; } const mutationEventModelDefinition = this.schema.namespaces[util_1.SYNC].models.MutationEvent; const userModelDefinition = this.schema.namespaces.user.models[head.model]; const recordId = (0, utils_1.getIdentifierValue)(userModelDefinition, record); const predicate = predicates_1.ModelPredicateCreator.createFromAST(mutationEventModelDefinition, { and: [ { modelId: { eq: recordId } }, { id: { ne: this.inProgressMutationEventId } }, ], }); const outdatedMutations = await storage.query(this._MutationEvent, predicate); if (!outdatedMutations.length) { return; } const reconciledMutations = outdatedMutations.map(m => { const oldData = JSON.parse(m.data); const newData = { ...oldData, _version, _lastChangedAt }; return this._MutationEvent.copyOf(m, draft => { draft.data = JSON.stringify(newData); }); }); await storage.delete(this._MutationEvent, predicate); await Promise.all(reconciledMutations.map(async (m) => storage.save(m, undefined, this.ownSymbol))); } mergeUserFields(previous, current) { const { _version, _lastChangedAt, _deleted, ...previousData } = JSON.parse(previous.data); const { _version: __version, _lastChangedAt: __lastChangedAt, _deleted: __deleted, ...currentData } = JSON.parse(current.data); const data = JSON.stringify({ _version, _lastChangedAt, _deleted, ...previousData, ...currentData, }); return this.modelInstanceCreator(this._MutationEvent, { ...current, data, }); } /* if a model is using custom timestamp fields the custom field names will be stored in the model attributes e.g. "attributes": [ { "type": "model", "properties": { "timestamps": { "createdAt": "createdOn", "updatedAt": "updatedOn" } } } ] */ removeTimestampFields(model, record) { const CREATED_AT_DEFAULT_KEY = 'createdAt'; const UPDATED_AT_DEFAULT_KEY = 'updatedAt'; let createdTimestampKey = CREATED_AT_DEFAULT_KEY; let updatedTimestampKey = UPDATED_AT_DEFAULT_KEY; const modelAttributes = this.schema.namespaces[util_1.USER].models[model].attributes?.find(attr => attr.type === 'model'); const timestampFieldsMap = modelAttributes?.properties?.timestamps; if (timestampFieldsMap) { createdTimestampKey = timestampFieldsMap[CREATED_AT_DEFAULT_KEY]; updatedTimestampKey = timestampFieldsMap[UPDATED_AT_DEFAULT_KEY]; } delete record[createdTimestampKey]; delete record[updatedTimestampKey]; return record; } } exports.MutationEventOutbox = MutationEventOutbox; //# sourceMappingURL=outbox.js.map