UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

589 lines • 40.5 kB
import { __assign, __awaiter, __generator, __read, __rest, __values } from "tslib"; import { API } from '@aws-amplify/api'; import { ConsoleLogger as Logger, jitteredBackoff, NonRetryableError, retry, BackgroundProcessManager, } from '@aws-amplify/core'; import Observable from 'zen-observable-ts'; import { DISCARD, isModelFieldType, isTargetNameAssociation, OpType, ProcessName, } from '../../types'; import { extractTargetNamesFromSrc, USER, USER_AGENT_SUFFIX_DATASTORE, ID, } from '../../util'; import { buildGraphQLOperation, createMutationInstanceFromModelOperation, getModelAuthModes, TransformerMutationType, getTokenForCustomAuth, } from '../utils'; import { getMutationErrorType } from './errorMaps'; var MAX_ATTEMPTS = 10; var logger = new Logger('DataStore'); var MutationProcessor = /** @class */ (function () { function MutationProcessor(schema, storage, userClasses, outbox, modelInstanceCreator, MutationEvent, amplifyConfig, authModeStrategy, errorHandler, conflictHandler, amplifyContext) { if (amplifyConfig === void 0) { amplifyConfig = {}; } this.schema = schema; this.storage = storage; this.userClasses = userClasses; this.outbox = outbox; this.modelInstanceCreator = modelInstanceCreator; this.MutationEvent = MutationEvent; this.amplifyConfig = amplifyConfig; this.authModeStrategy = authModeStrategy; this.errorHandler = errorHandler; this.conflictHandler = conflictHandler; this.amplifyContext = amplifyContext; this.typeQuery = new WeakMap(); this.processing = false; this.runningProcesses = new BackgroundProcessManager(); this.amplifyContext.API = this.amplifyContext.API || API; this.generateQueries(); } MutationProcessor.prototype.generateQueries = function () { var _this = this; Object.values(this.schema.namespaces).forEach(function (namespace) { Object.values(namespace.models) .filter(function (_a) { var syncable = _a.syncable; return syncable; }) .forEach(function (model) { var _a = __read(buildGraphQLOperation(namespace, model, 'CREATE'), 1), createMutation = _a[0]; var _b = __read(buildGraphQLOperation(namespace, model, 'UPDATE'), 1), updateMutation = _b[0]; var _c = __read(buildGraphQLOperation(namespace, model, 'DELETE'), 1), deleteMutation = _c[0]; _this.typeQuery.set(model, [ createMutation, updateMutation, deleteMutation, ]); }); }); }; MutationProcessor.prototype.isReady = function () { return this.observer !== undefined; }; MutationProcessor.prototype.start = function () { var _this = this; this.runningProcesses = new BackgroundProcessManager(); var observable = new Observable(function (observer) { _this.observer = observer; try { _this.resume(); } catch (error) { logger.error('mutations processor start error', error); throw error; } return _this.runningProcesses.addCleaner(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { // The observer has unsubscribed and/or `stop()` has been called. this.removeObserver(); this.pause(); return [2 /*return*/]; }); }); }); }); return observable; }; MutationProcessor.prototype.stop = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: this.removeObserver(); return [4 /*yield*/, this.runningProcesses.close()]; case 1: _a.sent(); return [4 /*yield*/, this.runningProcesses.open()]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; MutationProcessor.prototype.removeObserver = function () { var _a, _b; (_b = (_a = this.observer) === null || _a === void 0 ? void 0 : _a.complete) === null || _b === void 0 ? void 0 : _b.call(_a); this.observer = undefined; }; MutationProcessor.prototype.resume = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (this.runningProcesses.isOpen && this.runningProcesses.add(function (onTerminate) { return __awaiter(_this, void 0, void 0, function () { var head, namespaceName, _loop_1, this_1, _a; var _this = this; var _b, _c; return __generator(this, function (_d) { switch (_d.label) { case 0: if (this.processing || !this.isReady() || !this.runningProcesses.isOpen) { return [2 /*return*/]; } this.processing = true; namespaceName = USER; _loop_1 = function () { var model, operation, data, condition, modelConstructor, result, opName, modelDefinition, modelAuthModes, operationAuthModes_1, authModeAttempts_1, authModeRetry_1, error_1, record, hasMore; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: model = head.model, operation = head.operation, data = head.data, condition = head.condition; modelConstructor = this_1.userClasses[model]; result = undefined; opName = undefined; modelDefinition = undefined; _b.label = 1; case 1: _b.trys.push([1, 4, , 5]); return [4 /*yield*/, getModelAuthModes({ authModeStrategy: this_1.authModeStrategy, defaultAuthMode: this_1.amplifyConfig.aws_appsync_authenticationType, modelName: model, schema: this_1.schema, })]; case 2: modelAuthModes = _b.sent(); operationAuthModes_1 = modelAuthModes[operation.toUpperCase()]; authModeAttempts_1 = 0; authModeRetry_1 = function () { return __awaiter(_this, void 0, void 0, function () { var response, error_2, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 9]); logger.debug("Attempting mutation with authMode: " + operationAuthModes_1[authModeAttempts_1]); return [4 /*yield*/, this.jitteredRetry(namespaceName, model, operation, data, condition, modelConstructor, this.MutationEvent, head, operationAuthModes_1[authModeAttempts_1], onTerminate)]; case 1: response = _a.sent(); logger.debug("Mutation sent successfully with authMode: " + operationAuthModes_1[authModeAttempts_1]); return [2 /*return*/, response]; case 2: error_2 = _a.sent(); authModeAttempts_1++; if (!(authModeAttempts_1 >= operationAuthModes_1.length)) return [3 /*break*/, 7]; logger.debug("Mutation failed with authMode: " + operationAuthModes_1[authModeAttempts_1 - 1]); _a.label = 3; case 3: _a.trys.push([3, 5, , 6]); return [4 /*yield*/, this.errorHandler({ recoverySuggestion: 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', localModel: null, message: error_2.message, model: modelConstructor.name, operation: opName, errorType: getMutationErrorType(error_2), process: ProcessName.sync, remoteModel: null, cause: error_2, })]; case 4: _a.sent(); return [3 /*break*/, 6]; case 5: e_1 = _a.sent(); logger.error('Mutation error handler failed with:', e_1); return [3 /*break*/, 6]; case 6: throw error_2; case 7: logger.debug("Mutation failed with authMode: " + operationAuthModes_1[authModeAttempts_1 - 1] + ". Retrying with authMode: " + operationAuthModes_1[authModeAttempts_1]); return [4 /*yield*/, authModeRetry_1()]; case 8: return [2 /*return*/, _a.sent()]; case 9: return [2 /*return*/]; } }); }); }; return [4 /*yield*/, authModeRetry_1()]; case 3: _a = __read.apply(void 0, [_b.sent(), 3]), result = _a[0], opName = _a[1], modelDefinition = _a[2]; return [3 /*break*/, 5]; case 4: error_1 = _b.sent(); if (error_1.message === 'Offline' || error_1.message === 'RetryMutation') { return [2 /*return*/, "continue"]; } return [3 /*break*/, 5]; case 5: if (!(result === undefined)) return [3 /*break*/, 7]; logger.debug('done retrying'); return [4 /*yield*/, this_1.storage.runExclusive(function (storage) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.outbox.dequeue(storage)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); })]; case 6: _b.sent(); return [2 /*return*/, "continue"]; case 7: record = result.data[opName]; hasMore = false; return [4 /*yield*/, this_1.storage.runExclusive(function (storage) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // using runExclusive to prevent possible race condition // when another record gets enqueued between dequeue and peek return [4 /*yield*/, this.outbox.dequeue(storage, record, operation)]; case 1: // using runExclusive to prevent possible race condition // when another record gets enqueued between dequeue and peek _a.sent(); return [4 /*yield*/, this.outbox.peek(storage)]; case 2: hasMore = (_a.sent()) !== undefined; return [2 /*return*/]; } }); }); })]; case 8: _b.sent(); (_c = (_b = this_1.observer) === null || _b === void 0 ? void 0 : _b.next) === null || _c === void 0 ? void 0 : _c.call(_b, { operation: operation, modelDefinition: modelDefinition, model: record, hasMore: hasMore, }); return [2 /*return*/]; } }); }; this_1 = this; _d.label = 1; case 1: _a = this.processing && this.runningProcesses.isOpen; if (!_a) return [3 /*break*/, 3]; return [4 /*yield*/, this.outbox.peek(this.storage)]; case 2: _a = (head = _d.sent()) !== undefined; _d.label = 3; case 3: if (!_a) return [3 /*break*/, 5]; return [5 /*yield**/, _loop_1()]; case 4: _d.sent(); return [3 /*break*/, 1]; case 5: // pauses itself this.pause(); return [2 /*return*/]; } }); }); }, 'mutation resume loop'))]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; MutationProcessor.prototype.jitteredRetry = function (namespaceName, model, operation, data, condition, modelConstructor, MutationEvent, mutationEvent, authMode, onTerminate) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, retry(function (model, operation, data, condition, modelConstructor, MutationEvent, mutationEvent) { return __awaiter(_this, void 0, void 0, function () { var _a, query, variables, graphQLCondition, opName, modelDefinition, authToken, tryWith, attempt, opType, result, err_1, _b, error, _c, _d, code, retryWith, err_2, _e, _f, opName_1, query_1, authToken_1, serverData, namespace, updatedMutation; var _g; return __generator(this, function (_h) { switch (_h.label) { case 0: _a = __read(this.createQueryVariables(namespaceName, model, operation, data, condition), 5), query = _a[0], variables = _a[1], graphQLCondition = _a[2], opName = _a[3], modelDefinition = _a[4]; return [4 /*yield*/, getTokenForCustomAuth(authMode, this.amplifyConfig)]; case 1: authToken = _h.sent(); tryWith = { query: query, variables: variables, authMode: authMode, authToken: authToken, userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, }; attempt = 0; opType = this.opTypeFromTransformerOperation(operation); _h.label = 2; case 2: _h.trys.push([2, 4, , 17]); return [4 /*yield*/, this.amplifyContext.API.graphql(tryWith)]; case 3: result = (_h.sent()); // Use `as any` because TypeScript doesn't seem to like passing tuples // through generic params. return [2 /*return*/, [result, opName, modelDefinition]]; case 4: err_1 = _h.sent(); if (!(err_1.errors && err_1.errors.length > 0)) return [3 /*break*/, 15]; _b = __read(err_1.errors, 1), error = _b[0]; _c = error.originalError, _d = (_c === void 0 ? {} : _c).code, code = _d === void 0 ? null : _d; if (error.errorType === 'Unauthorized') { throw new NonRetryableError('Unauthorized'); } if (error.message === 'Network Error' || code === 'ECONNABORTED' // refers to axios timeout error caused by device's bad network condition ) { if (!this.processing) { throw new NonRetryableError('Offline'); } // TODO: Check errors on different env (react-native or other browsers) throw new Error('Network Error'); } if (!(error.errorType === 'ConflictUnhandled')) return [3 /*break*/, 13]; // TODO: add on ConflictConditionalCheck error query last from server attempt++; retryWith = void 0; if (!(attempt > MAX_ATTEMPTS)) return [3 /*break*/, 5]; retryWith = DISCARD; return [3 /*break*/, 8]; case 5: _h.trys.push([5, 7, , 8]); return [4 /*yield*/, this.conflictHandler({ modelConstructor: modelConstructor, localModel: this.modelInstanceCreator(modelConstructor, variables.input), remoteModel: this.modelInstanceCreator(modelConstructor, error.data), operation: opType, attempts: attempt, })]; case 6: retryWith = _h.sent(); return [3 /*break*/, 8]; case 7: err_2 = _h.sent(); logger.warn('conflict trycatch', err_2); return [3 /*break*/, 17]; case 8: if (!(retryWith === DISCARD)) return [3 /*break*/, 11]; _e = __read(buildGraphQLOperation(this.schema.namespaces[namespaceName], modelDefinition, 'GET'), 1), _f = __read(_e[0], 3), opName_1 = _f[1], query_1 = _f[2]; return [4 /*yield*/, getTokenForCustomAuth(authMode, this.amplifyConfig)]; case 9: authToken_1 = _h.sent(); return [4 /*yield*/, this.amplifyContext.API.graphql({ query: query_1, variables: { id: variables.input.id }, authMode: authMode, authToken: authToken_1, userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, })]; case 10: serverData = _h.sent(); // onTerminate cancel graphql() return [2 /*return*/, [serverData, opName_1, modelDefinition]]; case 11: namespace = this.schema.namespaces[namespaceName]; updatedMutation = createMutationInstanceFromModelOperation(namespace.relationships, modelDefinition, opType, modelConstructor, retryWith, graphQLCondition, MutationEvent, this.modelInstanceCreator, mutationEvent.id); return [4 /*yield*/, this.storage.save(updatedMutation)]; case 12: _h.sent(); throw new NonRetryableError('RetryMutation'); case 13: try { this.errorHandler({ recoverySuggestion: 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues', localModel: variables.input, message: error.message, operation: operation, errorType: getMutationErrorType(error), errorInfo: error.errorInfo, process: ProcessName.mutate, cause: error, remoteModel: error.data ? this.modelInstanceCreator(modelConstructor, error.data) : null, }); } catch (err) { logger.warn('Mutation error handler failed with:', err); } finally { // Return empty tuple, dequeues the mutation return [2 /*return*/, error.data ? [ { data: (_g = {}, _g[opName] = error.data, _g) }, opName, modelDefinition, ] : []]; } _h.label = 14; case 14: return [3 /*break*/, 16]; case 15: // Catch-all for client-side errors that don't come back in the `GraphQLError` format. // These errors should not be retried. throw new NonRetryableError(err_1); case 16: return [3 /*break*/, 17]; case 17: if (tryWith) return [3 /*break*/, 2]; _h.label = 18; case 18: return [2 /*return*/]; } }); }); }, [ model, operation, data, condition, modelConstructor, MutationEvent, mutationEvent, ], safeJitteredBackoff, onTerminate)]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; MutationProcessor.prototype.createQueryVariables = function (namespaceName, model, operation, data, condition) { var e_2, _a, e_3, _b, e_4, _c; var _d, _e; var modelDefinition = this.schema.namespaces[namespaceName].models[model]; var primaryKey = this.schema.namespaces[namespaceName].keys[model].primaryKey; var auth = (_d = modelDefinition.attributes) === null || _d === void 0 ? void 0 : _d.find(function (a) { return a.type === 'auth'; }); var ownerFields = ((_e = auth === null || auth === void 0 ? void 0 : auth.properties) === null || _e === void 0 ? void 0 : _e.rules.map(function (rule) { return rule.ownerField; }).filter(function (f) { return f; })) || ['owner']; var queriesTuples = this.typeQuery.get(modelDefinition); var _f = __read(queriesTuples.find(function (_a) { var _b = __read(_a, 1), transformerMutationType = _b[0]; return transformerMutationType === operation; }), 3), opName = _f[1], query = _f[2]; var _g = JSON.parse(data), _version = _g._version, parsedData = __rest(_g, ["_version"]); // include all the fields that comprise a custom PK if one is specified var deleteInput = {}; if (primaryKey === null || primaryKey === void 0 ? void 0 : primaryKey.length) { try { for (var primaryKey_1 = __values(primaryKey), primaryKey_1_1 = primaryKey_1.next(); !primaryKey_1_1.done; primaryKey_1_1 = primaryKey_1.next()) { var pkField = primaryKey_1_1.value; deleteInput[pkField] = parsedData[pkField]; } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (primaryKey_1_1 && !primaryKey_1_1.done && (_a = primaryKey_1.return)) _a.call(primaryKey_1); } finally { if (e_2) throw e_2.error; } } } else { deleteInput[ID] = parsedData.id; } var mutationInput; if (operation === TransformerMutationType.DELETE) { // For DELETE mutations, only the key(s) are included in the input mutationInput = deleteInput; } else { // Otherwise, we construct the mutation input with the following logic mutationInput = {}; var modelFields = Object.values(modelDefinition.fields); try { for (var modelFields_1 = __values(modelFields), modelFields_1_1 = modelFields_1.next(); !modelFields_1_1.done; modelFields_1_1 = modelFields_1.next()) { var _h = modelFields_1_1.value, name_1 = _h.name, type = _h.type, association = _h.association, isReadOnly = _h.isReadOnly; // omit readonly fields. cloud storage doesn't need them and won't take them! if (isReadOnly) { continue; } // omit owner fields if it's `null`. cloud storage doesn't allow it. if (ownerFields.includes(name_1) && parsedData[name_1] === null) { continue; } // model fields should be stripped out from the input if (isModelFieldType(type)) { // except for belongs to relations - we need to replace them with the correct foreign key(s) if (isTargetNameAssociation(association) && association.connectionType === 'BELONGS_TO') { var targetNames = extractTargetNamesFromSrc(association); if (targetNames) { try { // instead of including the connected model itself, we add its key(s) to the mutation input for (var targetNames_1 = (e_4 = void 0, __values(targetNames)), targetNames_1_1 = targetNames_1.next(); !targetNames_1_1.done; targetNames_1_1 = targetNames_1.next()) { var targetName = targetNames_1_1.value; mutationInput[targetName] = parsedData[targetName]; } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (targetNames_1_1 && !targetNames_1_1.done && (_c = targetNames_1.return)) _c.call(targetNames_1); } finally { if (e_4) throw e_4.error; } } } } continue; } // scalar fields / non-model types if (operation === TransformerMutationType.UPDATE) { if (!parsedData.hasOwnProperty(name_1)) { // for update mutations - strip out a field if it's unchanged continue; } } // all other fields are added to the input object mutationInput[name_1] = parsedData[name_1]; } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (modelFields_1_1 && !modelFields_1_1.done && (_b = modelFields_1.return)) _b.call(modelFields_1); } finally { if (e_3) throw e_3.error; } } } // Build mutation variables input object var input = __assign(__assign({}, mutationInput), { _version: _version }); var graphQLCondition = JSON.parse(condition); var variables = __assign({ input: input }, (operation === TransformerMutationType.CREATE ? {} : { condition: Object.keys(graphQLCondition).length > 0 ? graphQLCondition : null, })); return [query, variables, graphQLCondition, opName, modelDefinition]; }; MutationProcessor.prototype.opTypeFromTransformerOperation = function (operation) { switch (operation) { case TransformerMutationType.CREATE: return OpType.INSERT; case TransformerMutationType.DELETE: return OpType.DELETE; case TransformerMutationType.UPDATE: return OpType.UPDATE; case TransformerMutationType.GET: // Intentionally blank break; default: throw new Error("Invalid operation " + operation); } // because it makes TS happy ... return undefined; }; MutationProcessor.prototype.pause = function () { this.processing = false; }; return MutationProcessor; }()); var MAX_RETRY_DELAY_MS = 5 * 60 * 1000; var originalJitteredBackoff = jitteredBackoff(MAX_RETRY_DELAY_MS); /** * @private * Internal use of Amplify only. * * Wraps the jittered backoff calculation to retry Network Errors indefinitely. * Backs off according to original jittered retry logic until the original retry * logic hits its max. After this occurs, if the error is a Network Error, we * ignore the attempt count and return MAX_RETRY_DELAY_MS to retry forever (until * the request succeeds). * * @param attempt ignored * @param _args ignored * @param error tested to see if `.message` is 'Network Error' * @returns number | false : */ export var safeJitteredBackoff = function (attempt, _args, error) { var attemptResult = originalJitteredBackoff(attempt); // If this is the last attempt and it is a network error, we retry indefinitively every 5 minutes if (attemptResult === false && (error === null || error === void 0 ? void 0 : error.message) === 'Network Error') { return MAX_RETRY_DELAY_MS; } return attemptResult; }; export { MutationProcessor }; //# sourceMappingURL=mutation.js.map