UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

440 lines • 29.6 kB
import { __awaiter, __generator, __read, __values } from "tslib"; import { API } from '@aws-amplify/api'; import Observable from 'zen-observable-ts'; import { ProcessName, } from '../../types'; import { buildGraphQLOperation, getModelAuthModes, getClientSideAuthError, getForbiddenError, predicateToGraphQLFilter, getTokenForCustomAuth, } from '../utils'; import { USER_AGENT_SUFFIX_DATASTORE } from '../../util'; import { jitteredExponentialRetry, ConsoleLogger as Logger, Hub, NonRetryableError, BackgroundProcessManager, } from '@aws-amplify/core'; import { ModelPredicateCreator } from '../../predicates'; import { getSyncErrorType } from './errorMaps'; var opResultDefaults = { items: [], nextToken: null, startedAt: null, }; var logger = new Logger('DataStore'); var SyncProcessor = /** @class */ (function () { function SyncProcessor(schema, syncPredicates, amplifyConfig, authModeStrategy, errorHandler, amplifyContext) { if (amplifyConfig === void 0) { amplifyConfig = {}; } this.schema = schema; this.syncPredicates = syncPredicates; this.amplifyConfig = amplifyConfig; this.authModeStrategy = authModeStrategy; this.errorHandler = errorHandler; this.amplifyContext = amplifyContext; this.typeQuery = new WeakMap(); this.runningProcesses = new BackgroundProcessManager(); amplifyContext.API = amplifyContext.API || API; this.generateQueries(); } SyncProcessor.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, 'LIST'), 1), _b = __read(_a[0]), opNameQuery = _b.slice(1); _this.typeQuery.set(model, opNameQuery); }); }); }; SyncProcessor.prototype.graphqlFilterFromPredicate = function (model) { if (!this.syncPredicates) { return null; } var predicatesGroup = ModelPredicateCreator.getPredicates(this.syncPredicates.get(model), false); if (!predicatesGroup) { return null; } return predicateToGraphQLFilter(predicatesGroup); }; SyncProcessor.prototype.retrievePage = function (modelDefinition, lastSync, nextToken, limit, filter, onTerminate) { if (limit === void 0) { limit = null; } return __awaiter(this, void 0, void 0, function () { var _a, opName, query, variables, modelAuthModes, readAuthModes, authModeAttempts, authModeRetry, data, _b, _c, opResult, items, newNextToken, startedAt; var _this = this; return __generator(this, function (_d) { switch (_d.label) { case 0: _a = __read(this.typeQuery.get(modelDefinition), 2), opName = _a[0], query = _a[1]; variables = { limit: limit, nextToken: nextToken, lastSync: lastSync, filter: filter, }; return [4 /*yield*/, getModelAuthModes({ authModeStrategy: this.authModeStrategy, defaultAuthMode: this.amplifyConfig.aws_appsync_authenticationType, modelName: modelDefinition.name, schema: this.schema, })]; case 1: modelAuthModes = _d.sent(); readAuthModes = modelAuthModes.READ; authModeAttempts = 0; authModeRetry = function () { return __awaiter(_this, void 0, void 0, function () { var response, error_1, authMode; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!this.runningProcesses.isOpen) { throw new Error('sync.retreievePage termination was requested. Exiting.'); } _b.label = 1; case 1: _b.trys.push([1, 3, , 5]); logger.debug("Attempting sync with authMode: " + readAuthModes[authModeAttempts]); return [4 /*yield*/, this.jitteredRetry({ query: query, variables: variables, opName: opName, modelDefinition: modelDefinition, authMode: readAuthModes[authModeAttempts], onTerminate: onTerminate, })]; case 2: response = _b.sent(); logger.debug("Sync successful with authMode: " + readAuthModes[authModeAttempts]); return [2 /*return*/, response]; case 3: error_1 = _b.sent(); authModeAttempts++; if (authModeAttempts >= readAuthModes.length) { authMode = readAuthModes[authModeAttempts - 1]; logger.debug("Sync failed with authMode: " + authMode, error_1); if (getClientSideAuthError(error_1) || getForbiddenError(error_1)) { // return empty list of data so DataStore will continue to sync other models logger.warn("User is unauthorized to query " + opName + " with auth mode " + authMode + ". No data could be returned."); return [2 /*return*/, { data: (_a = {}, _a[opName] = opResultDefaults, _a), }]; } throw error_1; } logger.debug("Sync failed with authMode: " + readAuthModes[authModeAttempts - 1] + ". Retrying with authMode: " + readAuthModes[authModeAttempts]); return [4 /*yield*/, authModeRetry()]; case 4: return [2 /*return*/, _b.sent()]; case 5: return [2 /*return*/]; } }); }); }; return [4 /*yield*/, authModeRetry()]; case 2: data = (_d.sent()).data; _b = data, _c = opName, opResult = _b[_c]; items = opResult.items, newNextToken = opResult.nextToken, startedAt = opResult.startedAt; return [2 /*return*/, { nextToken: newNextToken, startedAt: startedAt, items: items, }]; } }); }); }; SyncProcessor.prototype.jitteredRetry = function (_a) { var query = _a.query, variables = _a.variables, opName = _a.opName, modelDefinition = _a.modelDefinition, authMode = _a.authMode, onTerminate = _a.onTerminate; return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, jitteredExponentialRetry(function (query, variables) { return __awaiter(_this, void 0, void 0, function () { var authToken, error_2, clientOrForbiddenErrorMessage, hasItems, unauthorized, otherErrors, result; var _this = this; var _a, _b, _c, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: _e.trys.push([0, 3, , 6]); return [4 /*yield*/, getTokenForCustomAuth(authMode, this.amplifyConfig)]; case 1: authToken = _e.sent(); return [4 /*yield*/, this.amplifyContext.API.graphql({ query: query, variables: variables, authMode: authMode, authToken: authToken, userAgentSuffix: USER_AGENT_SUFFIX_DATASTORE, })]; case 2: return [2 /*return*/, _e.sent()]; case 3: error_2 = _e.sent(); clientOrForbiddenErrorMessage = getClientSideAuthError(error_2) || getForbiddenError(error_2); if (clientOrForbiddenErrorMessage) { logger.error('Sync processor retry error:', error_2); throw new NonRetryableError(clientOrForbiddenErrorMessage); } hasItems = Boolean((_b = (_a = error_2 === null || error_2 === void 0 ? void 0 : error_2.data) === null || _a === void 0 ? void 0 : _a[opName]) === null || _b === void 0 ? void 0 : _b.items); unauthorized = (error_2 === null || error_2 === void 0 ? void 0 : error_2.errors) && error_2.errors.some(function (err) { return err.errorType === 'Unauthorized'; }); otherErrors = (error_2 === null || error_2 === void 0 ? void 0 : error_2.errors) && error_2.errors.filter(function (err) { return err.errorType !== 'Unauthorized'; }); result = error_2; if (hasItems) { result.data[opName].items = result.data[opName].items.filter(function (item) { return item !== null; }); } if (!(hasItems && (otherErrors === null || otherErrors === void 0 ? void 0 : otherErrors.length))) return [3 /*break*/, 5]; return [4 /*yield*/, Promise.all(otherErrors.map(function (err) { return __awaiter(_this, void 0, void 0, function () { var e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); 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: err.message, model: modelDefinition.name, operation: opName, errorType: getSyncErrorType(err), process: ProcessName.sync, remoteModel: null, cause: err, })]; case 1: _a.sent(); return [3 /*break*/, 3]; case 2: e_1 = _a.sent(); logger.error('Sync error handler failed with:', e_1); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }))]; case 4: _e.sent(); Hub.dispatch('datastore', { event: 'nonApplicableDataReceived', data: { errors: otherErrors, modelName: modelDefinition.name, }, }); _e.label = 5; case 5: /** * Handle $util.unauthorized() in resolver request mapper, which responses with something * like this: * * ``` * { * data: { syncYourModel: null }, * errors: [ * { * path: ['syncLegacyJSONComments'], * data: null, * errorType: 'Unauthorized', * errorInfo: null, * locations: [{ line: 2, column: 3, sourceName: null }], * message: * 'Not Authorized to access syncYourModel on type Query', * }, * ], * } * ``` * * The correct handling for this is to signal that we've encountered a non-retryable error, * since the server has responded with an auth error and *NO DATA* at this point. */ if (unauthorized) { 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: modelDefinition.name, operation: opName, errorType: getSyncErrorType(error_2.errors[0]), process: ProcessName.sync, remoteModel: null, cause: error_2, }); throw new NonRetryableError(error_2); } if ((_d = (_c = result.data) === null || _c === void 0 ? void 0 : _c[opName].items) === null || _d === void 0 ? void 0 : _d.length) { return [2 /*return*/, result]; } throw error_2; case 6: return [2 /*return*/]; } }); }); }, [query, variables], undefined, onTerminate)]; case 1: return [2 /*return*/, _b.sent()]; } }); }); }; SyncProcessor.prototype.start = function (typesLastSync) { var _this = this; var _a = this.amplifyConfig, maxRecordsToSync = _a.maxRecordsToSync, syncPageSize = _a.syncPageSize; var parentPromises = new Map(); var observable = new Observable(function (observer) { var sortedTypesLastSyncs = Object.values(_this.schema.namespaces).reduce(function (map, namespace) { var e_2, _a; try { for (var _b = __values(Array.from(namespace.modelTopologicalOrdering.keys())), _c = _b.next(); !_c.done; _c = _b.next()) { var modelName = _c.value; var typeLastSync = typesLastSync.get(namespace.models[modelName]); map.set(namespace.models[modelName], typeLastSync); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } return map; }, new Map()); var allModelsReady = Array.from(sortedTypesLastSyncs.entries()) .filter(function (_a) { var _b = __read(_a, 1), syncable = _b[0].syncable; return syncable; }) .map(function (_a) { var _b = __read(_a, 2), modelDefinition = _b[0], _c = __read(_b[1], 2), namespace = _c[0], lastSync = _c[1]; return _this.runningProcesses.isOpen && _this.runningProcesses.add(function (onTerminate) { return __awaiter(_this, void 0, void 0, function () { var done, nextToken, startedAt, items, recordsReceived, filter, parents, promises, promise; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: done = false; nextToken = null; startedAt = null; items = null; recordsReceived = 0; filter = this.graphqlFilterFromPredicate(modelDefinition); parents = this.schema.namespaces[namespace].modelTopologicalOrdering.get(modelDefinition.name); promises = parents.map(function (parent) { return parentPromises.get(namespace + "_" + parent); }); promise = new Promise(function (res) { return __awaiter(_this, void 0, void 0, function () { var limit, error_3, e_3; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, Promise.all(promises)]; case 1: _b.sent(); _b.label = 2; case 2: /** * If `runningProcesses` is not open, it means that the sync processor has been * stopped (for example by calling `DataStore.clear()` upstream) and has not yet * finished terminating and/or waiting for its background processes to complete. */ if (!this.runningProcesses.isOpen) { logger.debug("Sync processor has been stopped, terminating sync for " + modelDefinition.name); return [2 /*return*/, res()]; } limit = Math.min(maxRecordsToSync - recordsReceived, syncPageSize); _b.label = 3; case 3: _b.trys.push([3, 5, , 10]); return [4 /*yield*/, this.retrievePage(modelDefinition, lastSync, nextToken, limit, filter, onTerminate)]; case 4: (_a = _b.sent(), items = _a.items, nextToken = _a.nextToken, startedAt = _a.startedAt); return [3 /*break*/, 10]; case 5: error_3 = _b.sent(); _b.label = 6; case 6: _b.trys.push([6, 8, , 9]); 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_3.message, model: modelDefinition.name, operation: null, errorType: getSyncErrorType(error_3), process: ProcessName.sync, remoteModel: null, cause: error_3, })]; case 7: _b.sent(); return [3 /*break*/, 9]; case 8: e_3 = _b.sent(); logger.error('Sync error handler failed with:', e_3); return [3 /*break*/, 9]; case 9: /** * If there's an error, this model fails, but the rest of the sync should * continue. To facilitate this, we explicitly mark this model as `done` * with no items and allow the loop to continue organically. This ensures * all callbacks (subscription messages) happen as normal, so anything * waiting on them knows the model is as done as it can be. */ done = true; items = []; return [3 /*break*/, 10]; case 10: recordsReceived += items.length; done = nextToken === null || recordsReceived >= maxRecordsToSync; observer.next({ namespace: namespace, modelDefinition: modelDefinition, items: items, done: done, startedAt: startedAt, isFullSync: !lastSync, }); _b.label = 11; case 11: if (!done) return [3 /*break*/, 2]; _b.label = 12; case 12: res(); return [2 /*return*/]; } }); }); }); parentPromises.set(namespace + "_" + modelDefinition.name, promise); return [4 /*yield*/, promise]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }, "adding model " + modelDefinition.name); }); Promise.all(allModelsReady).then(function () { observer.complete(); }); }); return observable; }; SyncProcessor.prototype.stop = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: logger.debug('stopping sync processor'); return [4 /*yield*/, this.runningProcesses.close()]; case 1: _a.sent(); return [4 /*yield*/, this.runningProcesses.open()]; case 2: _a.sent(); logger.debug('sync processor stopped'); return [2 /*return*/]; } }); }); }; return SyncProcessor; }()); export { SyncProcessor }; //# sourceMappingURL=sync.js.map