@aws-amplify/datastore
Version:
AppSyncLocal support for aws-amplify
204 lines (201 loc) • 9.4 kB
JavaScript
'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