diff --git a/.eslintrc.js b/.eslintrc.js index b23a7841c83..a1b86acba3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,26 +54,6 @@ module.exports = { 'packages/rtn-push-notification/__tests__', 'packages/rtn-web-browser/__tests__', // 'packages/storage/__tests__', - // will enable lint by packages - // 'adapter-nextjs', - // 'packages/analytics', - // 'packages/api', - // 'packages/api-graphql', - // 'packages/api-rest', - // 'packages/auth', - // 'packages/aws-amplify', - // 'packages/core', - 'packages/datastore', - 'packages/datastore-storage-adapter', - // 'packages/geo', - // 'packages/interactions', - // 'packages/notifications', - // 'packages/predictions', - // 'packages/pubsub', - // 'packages/react-native', - // 'packages/rtn-push-notification', - // 'packages/rtn-web-browser', - // 'packages/storage', ], rules: { camelcase: [ diff --git a/packages/datastore-storage-adapter/package.json b/packages/datastore-storage-adapter/package.json index ceb169e7d60..44d8746d563 100644 --- a/packages/datastore-storage-adapter/package.json +++ b/packages/datastore-storage-adapter/package.json @@ -18,7 +18,8 @@ "build": "npm run clean && npm run build:esm-cjs && npm run build:umd", "clean": "rimraf dist lib lib-esm", "format": "echo \"Not implemented\"", - "lint": "tslint '{__tests__,src}/**/*.ts' && npm run ts-coverage", + "lint": "eslint '**/*.{ts,tsx}' && npm run ts-coverage", + "lint:fix": "eslint '**/*.{ts,tsx}' --fix", "ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 94.16" }, "repository": { diff --git a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts index c767ed80981..785e4bc8901 100644 --- a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts +++ b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteAdapter.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { CommonSQLiteAdapter } from '../common/CommonSQLiteAdapter'; + import ExpoSQLiteDatabase from './ExpoSQLiteDatabase'; const ExpoSQLiteAdapter: CommonSQLiteAdapter = new CommonSQLiteAdapter( diff --git a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts index b4536b5f838..4271f3b4387 100644 --- a/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts +++ b/packages/datastore-storage-adapter/src/ExpoSQLiteAdapter/ExpoSQLiteDatabase.ts @@ -3,7 +3,8 @@ import { ConsoleLogger } from '@aws-amplify/core'; import { PersistentModel } from '@aws-amplify/datastore'; import { deleteAsync, documentDirectory } from 'expo-file-system'; -import { openDatabase, WebSQLDatabase } from 'expo-sqlite'; +import { WebSQLDatabase, openDatabase } from 'expo-sqlite'; + import { DB_NAME } from '../common/constants'; import { CommonSQLiteDatabase, ParameterizedStatement } from '../common/types'; @@ -11,9 +12,9 @@ const logger = new ConsoleLogger('ExpoSQLiteDatabase'); /* -Note: -ExpoSQLite transaction error callbacks require returning a boolean value to indicate whether the -error was handled or not. Returning a true value indicates the error was handled and does not +Note: +ExpoSQLite transaction error callbacks require returning a boolean value to indicate whether the +error was handled or not. Returning a true value indicates the error was handled and does not rollback the whole transaction. */ @@ -56,6 +57,7 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { params: (string | number)[], ): Promise { const results: T[] = await this.getAll(statement, params); + return results[0]; } @@ -74,6 +76,7 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { (_, error) => { reject(error); logger.warn(error); + return true; }, ); @@ -93,6 +96,7 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { (_, error) => { reject(error); logger.warn(error); + return true; }, ); @@ -101,24 +105,27 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { } public batchQuery( - queryParameterizedStatements: Set = new Set(), + queryParameterizedStatements = new Set(), ): Promise { - return new Promise((resolveTransaction, rejectTransaction) => { + return new Promise((resolve, reject) => { + const resolveTransaction = resolve; + const rejectTransaction = reject; this.db.transaction(async transaction => { try { const results: any[] = await Promise.all( [...queryParameterizedStatements].map( ([statement, params]) => - new Promise((resolve, reject) => { + new Promise((_resolve, _reject) => { transaction.executeSql( statement, params, (_, result) => { - resolve(result.rows._array[0]); + _resolve(result.rows._array[0]); }, (_, error) => { - reject(error); + _reject(error); logger.warn(error); + return true; }, ); @@ -135,26 +142,29 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { } public batchSave( - saveParameterizedStatements: Set = new Set(), + saveParameterizedStatements = new Set(), deleteParameterizedStatements?: Set, ): Promise { - return new Promise((resolveTransaction, rejectTransaction) => { + return new Promise((resolve, reject) => { + const resolveTransaction = resolve; + const rejectTransaction = reject; this.db.transaction(async transaction => { try { // await for all sql statements promises to resolve await Promise.all( [...saveParameterizedStatements].map( ([statement, params]) => - new Promise((resolve, reject) => { + new Promise((_resolve, _reject) => { transaction.executeSql( statement, params, () => { - resolve(null); + _resolve(null); }, (_, error) => { - reject(error); + _reject(error); logger.warn(error); + return true; }, ); @@ -165,20 +175,21 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { await Promise.all( [...deleteParameterizedStatements].map( ([statement, params]) => - new Promise((resolve, reject) => + new Promise((_resolve, _reject) => { transaction.executeSql( statement, params, () => { - resolve(null); + _resolve(null); }, (_, error) => { - reject(error); + _reject(error); logger.warn(error); + return true; }, - ), - ), + ); + }), ), ); } @@ -198,33 +209,37 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { const [queryStatement, queryParams] = queryParameterizedStatement; const [deleteStatement, deleteParams] = deleteParameterizedStatement; - return new Promise((resolveTransaction, rejectTransaction) => { + return new Promise((resolve, reject) => { + const resolveTransaction = resolve; + const rejectTransaction = reject; this.db.transaction(async transaction => { try { - const result: T[] = await new Promise((resolve, reject) => { + const result: T[] = await new Promise((_resolve, _reject) => { transaction.executeSql( queryStatement, queryParams, - (_, result) => { - resolve(result.rows._array || []); + (_, sqlResult) => { + _resolve(sqlResult.rows._array || []); }, (_, error) => { - reject(error); + _reject(error); logger.warn(error); + return true; }, ); }); - await new Promise((resolve, reject) => { + await new Promise((_resolve, _reject) => { transaction.executeSql( deleteStatement, deleteParams, () => { - resolve(null); + _resolve(null); }, (_, error) => { - reject(error); + _reject(error); logger.warn(error); + return true; }, ); @@ -239,21 +254,24 @@ class ExpoSQLiteDatabase implements CommonSQLiteDatabase { } private executeStatements(statements: string[]): Promise { - return new Promise((resolveTransaction, rejectTransaction) => { + return new Promise((resolve, reject) => { + const resolveTransaction = resolve; + const rejectTransaction = reject; this.db.transaction(async transaction => { try { await Promise.all( statements.map( statement => - new Promise((resolve, reject) => { + new Promise((_resolve, _reject) => { transaction.executeSql( statement, [], () => { - resolve(null); + _resolve(null); }, (_, error) => { - reject(error); + _reject(error); + return true; }, ); diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts index b84e39ffb47..8d2823f0f05 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts +++ b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { CommonSQLiteAdapter } from '../common/CommonSQLiteAdapter'; + import SQLiteDatabase from './SQLiteDatabase'; const SQLiteAdapter: CommonSQLiteAdapter = new CommonSQLiteAdapter( diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts index 869f8be816f..c43483df97b 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts +++ b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteDatabase.ts @@ -3,6 +3,7 @@ import SQLite from 'react-native-sqlite-storage'; import { ConsoleLogger } from '@aws-amplify/core'; import { PersistentModel } from '@aws-amplify/datastore'; + import { DB_NAME } from '../common/constants'; import { CommonSQLiteDatabase, ParameterizedStatement } from '../common/types'; @@ -16,7 +17,7 @@ if (ConsoleLogger.LOG_LEVEL === 'DEBUG') { /* -Note: +Note: I purposely avoided using arrow functions () => {} in this class, Because I ran into issues with them in some of the SQLite method callbacks @@ -41,7 +42,7 @@ class SQLiteDatabase implements CommonSQLiteDatabase { } public async createSchema(statements: string[]): Promise { - return await this.executeStatements(statements); + await this.executeStatements(statements); } public async clear(): Promise { @@ -56,6 +57,7 @@ class SQLiteDatabase implements CommonSQLiteDatabase { params: (string | number)[], ): Promise { const results: T[] = await this.getAll(statement, params); + return results[0]; } @@ -138,7 +140,14 @@ class SQLiteDatabase implements CommonSQLiteDatabase { }, logger.warn, ); - tx.executeSql(deleteStatement, deleteParams, () => {}, logger.warn); + tx.executeSql( + deleteStatement, + deleteParams, + () => { + // no-op + }, + logger.warn, + ); }); return results; diff --git a/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts b/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts index 1caab5f0c8a..e0f49b0c591 100644 --- a/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts +++ b/packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts @@ -2,26 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger } from '@aws-amplify/core'; import { - generateSchemaStatements, - queryByIdStatement, - modelUpdateStatement, - modelInsertStatement, - queryAllStatement, - queryOneStatement, - deleteByIdStatement, - deleteByPredicateStatement, -} from '../common/SQLiteUtils'; - -import { - StorageAdapter, + InternalSchema, ModelInstanceCreator, + ModelPredicate, ModelPredicateCreator, ModelSortPredicateCreator, - InternalSchema, - isPredicateObj, - ModelPredicate, - NamespaceResolver, NAMESPACES, + NamespaceResolver, OpType, PaginationInput, PersistentModel, @@ -29,12 +16,26 @@ import { PredicateObject, PredicatesGroup, QueryOne, + StorageAdapter, + isPredicateObj, utils, } from '@aws-amplify/datastore'; + +import { + deleteByIdStatement, + deleteByPredicateStatement, + generateSchemaStatements, + modelInsertStatement, + modelUpdateStatement, + queryAllStatement, + queryByIdStatement, + queryOneStatement, +} from '../common/SQLiteUtils'; + import { CommonSQLiteDatabase, - ParameterizedStatement, ModelInstanceMetadataWithId, + ParameterizedStatement, } from './types'; const { traverseModel, validatePredicate, isModelConstructor } = utils; @@ -49,6 +50,7 @@ export class CommonSQLiteAdapter implements StorageAdapter { namsespaceName: string, modelName: string, ) => PersistentModelConstructor; + private db: CommonSQLiteDatabase; private initPromise: Promise; private resolve: (value?: any) => void; @@ -68,12 +70,13 @@ export class CommonSQLiteAdapter implements StorageAdapter { ) => PersistentModelConstructor, ) { if (!this.initPromise) { - this.initPromise = new Promise((res, rej) => { - this.resolve = res; - this.reject = rej; + this.initPromise = new Promise((_resolve, _reject) => { + this.resolve = _resolve; + this.reject = _reject; }); } else { await this.initPromise; + return; } this.schema = theSchema; @@ -86,6 +89,7 @@ export class CommonSQLiteAdapter implements StorageAdapter { this.schema.namespaces.user.models, ).some(model => Object.values(model.fields).some(field => + // eslint-disable-next-line no-prototype-builtins field.association?.hasOwnProperty('targetNames'), ), ); @@ -155,13 +159,19 @@ export class CommonSQLiteAdapter implements StorageAdapter { const { modelName, item, instance } = resItem; const { id } = item; - const [queryStatement, params] = queryByIdStatement(id, modelName); - const fromDB = await this.db.get(queryStatement, params); + const [queryStatementForRestItem, paramsForRestItem] = queryByIdStatement( + id, + modelName, + ); + const fromDBForRestItem = await this.db.get( + queryStatementForRestItem, + paramsForRestItem, + ); const opType: OpType = - fromDB === undefined ? OpType.INSERT : OpType.UPDATE; + fromDBForRestItem === undefined ? OpType.INSERT : OpType.UPDATE; - const saveStatement = fromDB + const saveStatement = fromDBForRestItem ? modelUpdateStatement(instance, modelName) : modelInsertStatement(instance, modelName); @@ -205,6 +215,7 @@ export class CommonSQLiteAdapter implements StorageAdapter { for (const r of relations) { delete record[r.fieldName]; } + return this.modelInstanceCreator(modelConstructor, record); }); } @@ -228,9 +239,10 @@ export class CommonSQLiteAdapter implements StorageAdapter { const queryById = predicates && this.idFromPredicate(predicates); - const records: T[] = await (async () => { + const records: T[] = (await (async () => { if (queryById) { const record = await this.getById(tableName, queryById); + return record ? [record] : []; } @@ -242,10 +254,10 @@ export class CommonSQLiteAdapter implements StorageAdapter { page, ); - return await this.db.getAll(queryStatement, params); - })(); + return this.db.getAll(queryStatement, params); + })()) as T[]; - return await this.load(namespaceName, modelConstructor.name, records); + return this.load(namespaceName, modelConstructor.name, records); } private async getById( @@ -396,14 +408,15 @@ export class CommonSQLiteAdapter implements StorageAdapter { const { id, _deleted } = item; const { instance } = connectedModels.find( - ({ instance }) => instance.id === id, + ({ instance: connectedModelInstance }) => + connectedModelInstance.id === id, ); if (_deleted) { // create the delete statements right away const deleteStatement = deleteByIdStatement(instance.id, tableName); deleteStatements.add(deleteStatement); - result.push([(item), OpType.DELETE]); + result.push([item as unknown as T, OpType.DELETE]); } else { // query statements for the saves at first const queryStatement = queryByIdStatement(id, tableName); @@ -423,14 +436,14 @@ export class CommonSQLiteAdapter implements StorageAdapter { tableName, ); saveStatements.add(insertStatement); - result.push([(itemsToSave[idx]), OpType.INSERT]); + result.push([itemsToSave[idx] as unknown as T, OpType.INSERT]); } else { const updateStatement = modelUpdateStatement( itemsToSave[idx], tableName, ); saveStatements.add(updateStatement); - result.push([(itemsToSave[idx]), OpType.UPDATE]); + result.push([itemsToSave[idx] as unknown as T, OpType.UPDATE]); } }); diff --git a/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts b/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts index 431b4918fff..c89cfc49bb6 100644 --- a/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts +++ b/packages/datastore-storage-adapter/src/common/SQLiteUtils.ts @@ -1,24 +1,24 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { + GraphQLScalarType, InternalSchema, - SchemaModel, + ModelAttributeAuth, + ModelAuthRule, ModelField, PersistentModel, - isGraphQLScalarType, - QueryOne, + PredicateObject, PredicatesGroup, - isPredicateObj, + QueryOne, + SchemaModel, SortPredicatesGroup, - PredicateObject, - isPredicateGroup, + isGraphQLScalarType, + isModelAttributeAuth, isModelFieldType, + isPredicateGroup, + isPredicateObj, isTargetNameAssociation, - isModelAttributeAuth, - ModelAttributeAuth, - ModelAuthRule, utils, - GraphQLScalarType, } from '@aws-amplify/datastore'; import { ParameterizedStatement } from './types'; @@ -43,6 +43,7 @@ const updateSet: (model: any) => [any, any] = model => { .filter(([k]) => k !== 'id') .map(([k, v]) => { values.push(prepareValueForDML(v)); + return `"${k}"=?`; }) .join(', '); @@ -97,9 +98,10 @@ export function getSQLiteType( return 'TEXT'; case 'Float': return 'REAL'; - default: + default: { const _: never = scalar as never; throw new Error(`unknown type ${scalar as string}`); + } } } @@ -136,13 +138,14 @@ export const implicitAuthFieldsForModel: (model: SchemaModel) => string[] = ( const authFieldExplicitlyDefined = Object.values(model.fields).find( (f: ModelField) => f.name === authField, ); + return !authFieldExplicitlyDefined; }); }; export function modelCreateTableStatement( model: SchemaModel, - userModel: boolean = false, + userModel = false, ): string { // implicitly defined auth fields, e.g., `owner`, `groupsField`, etc. const implicitAuthFields = implicitAuthFieldsForModel(model); @@ -210,6 +213,7 @@ export function modelCreateTableStatement( const createTableStatement = `CREATE TABLE IF NOT EXISTS "${ model.name }" (${fields.join(', ')});`; + return createTableStatement; } @@ -316,7 +320,7 @@ export const whereConditionFromPredicateObject = ({ return [`"${field}" ${comparisonOperator} ?`, [operand]]; } - const logicalOperatorKey = operator; + const logicalOperatorKey = operator as keyof typeof logicalOperatorMap; const logicalOperator = logicalOperatorMap[logicalOperatorKey]; @@ -339,10 +343,11 @@ export const whereConditionFromPredicateObject = ({ case 'notContains': statement = [`instr("${field}", ?) ${logicalOperator}`, [operand]]; break; - default: + default: { const _: never = logicalOperatorKey; // Incorrect WHERE clause can result in data loss throw new Error('Cannot map predicate to a valid WHERE clause'); + } } return statement; @@ -361,13 +366,14 @@ export function whereClauseFromPredicate( return [whereClause, params]; function recurse( - predicate: PredicatesGroup | PredicateObject, - result = [], - params = [], + recursedPredicate: PredicatesGroup | PredicateObject, + recursedResult = [], + recursedParams = [], ): void { - if (isPredicateGroup(predicate)) { - const { type: groupType, predicates: groupPredicates } = predicate; - let filterType: string = ''; + if (isPredicateGroup(recursedPredicate)) { + const { type: groupType, predicates: groupPredicates } = + recursedPredicate; + let filterType = ''; let isNegation = false; switch (groupType) { case 'not': @@ -379,25 +385,26 @@ export function whereClauseFromPredicate( case 'or': filterType = 'OR'; break; - default: + default: { const _: never = groupType as never; throw new Error(`Invalid ${groupType}`); + } } const groupResult = []; for (const p of groupPredicates) { - recurse(p, groupResult, params); + recurse(p, groupResult, recursedParams); } - result.push( + recursedResult.push( `${isNegation ? 'NOT' : ''}(${groupResult.join(` ${filterType} `)})`, ); - } else if (isPredicateObj(predicate)) { + } else if (isPredicateObj(recursedPredicate)) { const [condition, conditionParams] = - whereConditionFromPredicateObject(predicate); + whereConditionFromPredicateObject(recursedPredicate); - result.push(condition); + recursedResult.push(condition); - params.push(...conditionParams); + recursedParams.push(...conditionParams); } } } @@ -423,7 +430,7 @@ export function orderByClauseFromSort( export function limitClauseFromPagination( limit: number, - page: number = 0, + page = 0, ): ParameterizedStatement { const params = [limit]; let clause = 'LIMIT ?'; @@ -483,6 +490,7 @@ export function deleteByIdStatement( tableName: string, ): ParameterizedStatement { const deleteStatement = `DELETE FROM "${tableName}" WHERE "id"=?`; + return [deleteStatement, [id]]; } @@ -498,5 +506,6 @@ export function deleteByPredicateStatement( statement += ` ${whereClause}`; params.push(...whereParams); } + return [statement, params]; } diff --git a/packages/datastore-storage-adapter/src/common/types.ts b/packages/datastore-storage-adapter/src/common/types.ts index 2957233176b..c1bce70b876 100644 --- a/packages/datastore-storage-adapter/src/common/types.ts +++ b/packages/datastore-storage-adapter/src/common/types.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PersistentModel, ModelInstanceMetadata } from '@aws-amplify/datastore'; +import { ModelInstanceMetadata, PersistentModel } from '@aws-amplify/datastore'; export interface CommonSQLiteDatabase { init(): Promise; diff --git a/packages/datastore-storage-adapter/src/index.ts b/packages/datastore-storage-adapter/src/index.ts index 064a0f2b12c..19ba93a509b 100644 --- a/packages/datastore-storage-adapter/src/index.ts +++ b/packages/datastore-storage-adapter/src/index.ts @@ -1,4 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import SQLiteAdapter from './SQLiteAdapter/SQLiteAdapter'; + export { SQLiteAdapter }; diff --git a/packages/datastore-storage-adapter/tslint.json b/packages/datastore-storage-adapter/tslint.json deleted file mode 100644 index 8eafab1d2b4..00000000000 --- a/packages/datastore-storage-adapter/tslint.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "defaultSeverity": "error", - "plugins": ["prettier"], - "extends": [], - "jsRules": {}, - "rules": { - "prefer-const": true, - "max-line-length": [true, 120], - "no-empty-interface": true, - "no-var-keyword": true, - "object-literal-shorthand": true, - "no-eval": true, - "space-before-function-paren": [ - true, - { - "anonymous": "never", - "named": "never" - } - ], - "no-parameter-reassignment": true, - "align": [true, "parameters"], - "no-duplicate-imports": true, - "one-variable-per-declaration": [false, "ignore-for-loop"], - "triple-equals": [true, "allow-null-check"], - "comment-format": [true, "check-space"], - "indent": [false], - "whitespace": [ - false, - "check-branch", - "check-decl", - "check-operator", - "check-preblock" - ], - "eofline": true, - "variable-name": [ - true, - "check-format", - "allow-pascal-case", - "allow-snake-case", - "allow-leading-underscore" - ], - "semicolon": [ - true, - "always", - "ignore-interfaces", - "ignore-bound-class-methods" - ] - }, - "rulesDirectory": [] -} diff --git a/packages/datastore/package.json b/packages/datastore/package.json index c35621eacab..43b726a77f5 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -24,7 +24,8 @@ "clean": "npm run clean:size && rimraf dist lib lib-esm", "clean:size": "rimraf dual-publish-tmp tmp*", "format": "echo \"Not implemented\" && npm run ts-coverage", - "lint": "tslint '{__tests__,src}/**/*.ts' && npm run ts-coverage", + "lint": "eslint '**/*.{ts,tsx}' && npm run ts-coverage", + "lint:fix": "eslint '**/*.{ts,tsx}' --fix", "ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 92.05" }, "repository": { diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index 14ac0a1dbc3..c850b59da5c 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -1,14 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { fetchAuthSession } from '@aws-amplify/core'; +import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; + import { + AmplifyContext, AuthModeStrategy, + ModelAttributeAuthAllow, ModelAttributeAuthProperty, ModelAttributeAuthProvider, - ModelAttributeAuthAllow, - AmplifyContext, } from '../types'; -import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; function getProviderFromRule( rule: ModelAttributeAuthProperty, @@ -21,6 +22,7 @@ function getProviderFromRule( if (rule.allow === 'public' && !rule.provider) { return ModelAttributeAuthProvider.API_KEY; } + return rule.provider!; } @@ -48,6 +50,7 @@ function sortAuthRulesWithPriority(rules: ModelAttributeAuthProperty[]) { providerSortPriority.indexOf(getProviderFromRule(b)) ); } + return ( allowSortPriority.indexOf(a.allow) - allowSortPriority.indexOf(b.allow) ); @@ -138,7 +141,7 @@ function getAuthRules({ export const multiAuthStrategy: ( amplifyContext: AmplifyContext, ) => AuthModeStrategy = - (amplifyContext: AmplifyContext) => + () => async ({ schema, modelName }) => { let currentUser; try { @@ -164,5 +167,6 @@ export const multiAuthStrategy: ( return getAuthRules({ currentUser, rules: sortedRules }); } } + return []; }; diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 8e77e85b2e7..8882e9a4446 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -1,110 +1,108 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { InternalAPI } from '@aws-amplify/api/internals'; -import { Amplify, Hub, Cache, ConsoleLogger } from '@aws-amplify/core'; - +import { Amplify, Cache, ConsoleLogger, Hub } from '@aws-amplify/core'; import { Draft, + Patch, + enablePatches, immerable, produce, setAutoFreeze, - enablePatches, - Patch, } from 'immer'; -import { amplifyUuid, isBrowser } from '@aws-amplify/core/internals/utils'; +import { + BackgroundProcessManager, + amplifyUuid, +} from '@aws-amplify/core/internals/utils'; import { Observable, SubscriptionLike, filter } from 'rxjs'; + import { defaultAuthStrategy, multiAuthStrategy } from '../authModeStrategies'; import { - isPredicatesAll, ModelPredicateCreator, ModelSortPredicateCreator, PredicateAll, + isPredicatesAll, } from '../predicates'; import { Adapter } from '../storage/adapter'; import { ExclusiveStorage as Storage } from '../storage/storage'; import { ModelRelationship } from '../storage/relationship'; import { ControlMessage, SyncEngine } from '../sync'; import { + AmplifyContext, AuthModeStrategy, + AuthModeStrategyType, ConflictHandler, DataStoreConfig, + DataStoreSnapshot, + ErrorHandler, GraphQLScalarType, + IdentifierFieldOrIdentifierObject, InternalSchema, - isGraphQLScalarType, - isSchemaModelWithAttributes, + ManagedIdentifier, ModelFieldType, ModelInit, ModelInstanceMetadata, ModelPredicate, - ModelField, - SortPredicate, + ModelPredicateExtender, MutableModel, NamespaceResolver, NonModelTypeConstructor, - ProducerPaginationInput, + ObserveQueryOptions, PaginationInput, PersistentModel, PersistentModelConstructor, - ProducerModelPredicate, + PersistentModelMetaData, + ProducerPaginationInput, + RecursiveModelPredicateExtender, Schema, SchemaModel, SchemaNamespace, SchemaNonModel, + SortPredicate, SubscriptionMessage, - DataStoreSnapshot, SyncConflict, SyncError, - TypeConstructorMap, - ErrorHandler, SyncExpression, - AuthModeStrategyType, - isNonModelFieldType, - isModelFieldType, - ObserveQueryOptions, - ManagedIdentifier, - PersistentModelMetaData, - IdentifierFieldOrIdentifierObject, + TypeConstructorMap, + isGraphQLScalarType, isIdentifierObject, - AmplifyContext, - isFieldAssociation, - RecursiveModelPredicateExtender, - ModelPredicateExtender, + isModelFieldType, + isNonModelFieldType, + isSchemaModelWithAttributes, } from '../types'; -// tslint:disable:no-duplicate-imports import type { __modelMeta__ } from '../types'; -import { isNode } from './utils'; - import { DATASTORE, - errorMessages, - establishRelationAndKeys, - isModelConstructor, - monotonicUlidFactory, + DeferredCallbackResolver, NAMESPACES, STORAGE, SYNC, USER, - isNullOrUndefined, - registerNonModelClass, - sortCompareFunction, - DeferredCallbackResolver, - inMemoryPagination, + errorMessages, + establishRelationAndKeys, extractPrimaryKeyFieldNames, extractPrimaryKeysAndValues, + getTimestampFields, + inMemoryPagination, isIdManaged, isIdOptionallyManaged, + isModelConstructor, + isNullOrUndefined, mergePatches, - getTimestampFields, + monotonicUlidFactory, + registerNonModelClass, + sortCompareFunction, } from '../util'; import { - recursivePredicateFor, - predicateFor, GroupCondition, internals, + predicateFor, + recursivePredicateFor, } from '../predicates/next'; import { getIdentifierValue } from '../sync/utils'; import DataStoreConnectivity from '../sync/datastoreConnectivity'; -import { BackgroundProcessManager } from '@aws-amplify/core/internals/utils'; + +import { isNode } from './utils'; setAutoFreeze(true); enablePatches(); @@ -113,10 +111,10 @@ const logger = new ConsoleLogger('DataStore'); const ulid = monotonicUlidFactory(Date.now()); -type SettingMetaData = { +interface SettingMetaData { identifier: ManagedIdentifier; readOnlyFields: never; -}; +} declare class Setting { public readonly [__modelMeta__]: SettingMetaData; constructor(init: ModelInit); @@ -124,6 +122,7 @@ declare class Setting { src: Setting, mutator: (draft: MutableModel) => void | Setting, ): Setting; + public readonly id: string; public readonly key: string; public readonly value: string; @@ -178,6 +177,7 @@ const namespaceResolver: NamespaceResolver = modelConstructor => { `Namespace Resolver for '${modelConstructor.name}' not found! This is probably a bug in '@amplify-js/datastore'.`, ); } + return resolver; }; @@ -221,6 +221,8 @@ const buildSeedPredicate = ( }; // exporting syncClasses for testing outbox.test.ts +// TODO(eslint): refactor not to export non-constant +// eslint-disable-next-line import/no-mutable-exports export let syncClasses: TypeConstructorMap; let userClasses: TypeConstructorMap; let dataStoreClasses: TypeConstructorMap; @@ -282,6 +284,7 @@ export function attached( } else { result && attachedModelInstances.set(result, attachment); } + return result; } @@ -355,10 +358,10 @@ const initSchema = (userSchema: Schema) => { field => field.association && field.association.connectionType === 'BELONGS_TO' && - (field.type).model !== model.name, + (field.type as ModelFieldType).model !== model.name, ) .forEach(field => - connectedModels.push((field.type).model), + connectedModels.push((field.type as ModelFieldType).model), ); modelAssociations.set(model.name, connectedModels); @@ -367,7 +370,7 @@ const initSchema = (userSchema: Schema) => { // (such as predicate builders) don't have to reach back into "DataStore" space // to go looking for it. Object.values(model.fields).forEach(field => { - const relatedModel = userClasses[(field.type).model]; + const relatedModel = userClasses[(field.type as ModelFieldType).model]; if (isModelConstructor(relatedModel)) { Object.defineProperty(field.type, 'modelConstructor', { get: () => { @@ -376,6 +379,7 @@ const initSchema = (userSchema: Schema) => { throw new Error( `Could not find model definition for ${relatedModel.name}`, ); + return { builder: relatedModel, schema: relatedModelDefinition, @@ -390,8 +394,8 @@ const initSchema = (userSchema: Schema) => { // index fields into the model definition. // definition.cloudFields = { ...definition.fields }; - const indexes = - schema.namespaces[namespace].relationships![model.name].indexes; + const { indexes } = + schema.namespaces[namespace].relationships![model.name]; const indexFields = new Set(); for (const index of indexes) { @@ -488,7 +492,7 @@ const checkSchemaCodegenVersion = (codegenVersion: string) => { let isValid = false; try { const versionParts = codegenVersion.split('.'); - const [major, minor, patch, patchrevision] = versionParts; + const [major, minor] = versionParts; isValid = Number(major) === majorVersion && Number(minor) >= minorVersion; } catch (err) { console.log(`Error parsing codegen version: ${codegenVersion}\n${err}`); @@ -546,12 +550,12 @@ export declare type ModelInstanceCreator = typeof modelInstanceCreator; const instancesMetadata = new WeakSet>(); function modelInstanceCreator( - modelConstructor: PersistentModelConstructor, + ModelConstructor: PersistentModelConstructor, init: Partial, ): T { instancesMetadata.add(init); - return new modelConstructor(>>init); + return new ModelConstructor(init as ModelInit>); } const validateModelFields = @@ -597,6 +601,7 @@ const validateModelFields = if (typeof v === 'string') { try { JSON.parse(v); + return; } catch (error) { throw new Error(`Field ${name} is an invalid JSON object. ${v}`); @@ -618,11 +623,11 @@ const validateModelFields = if ( !isNullOrUndefined(v) && - (<[]>v).some(e => + (v as []).some(e => isNullOrUndefined(e) ? isRequired : typeof e !== jsType, ) ) { - const elemTypes = (<[]>v) + const elemTypes = (v as []) .map(e => (e === null ? 'null' : typeof e)) .join(','); @@ -632,7 +637,7 @@ const validateModelFields = } if (validateScalar && !isNullOrUndefined(v)) { - const validationStatus = (<[]>v).map(e => { + const validationStatus = (v as []).map(e => { if (!isNullOrUndefined(e)) { return validateScalar(e); } else if (isNullOrUndefined(e) && !isRequired) { @@ -649,7 +654,7 @@ const validateModelFields = } } } else if (!isRequired && v === undefined) { - return; + // no-op for this branch but still to filter this branch out } else if (typeof v !== jsType && v !== null) { throw new Error( `Field ${name} should be of type ${jsType}, ${typeof v} received. ${v}`, @@ -771,7 +776,7 @@ const initializeInstance = ( const parsedValue = castInstanceType(modelDefinition, k, v); modelValidator(k, parsedValue); - (draft)[k] = parsedValue; + (draft as any)[k] = parsedValue; }); }; @@ -799,14 +804,14 @@ const normalize = ( draft: Draft, ) => { for (const k of Object.keys(modelDefinition.fields)) { - if (draft[k] === undefined) (draft)[k] = null; + if (draft[k] === undefined) (draft as any)[k] = null; } }; const createModelClass = ( modelDefinition: SchemaModel, ) => { - const clazz = >(class Model { + const clazz = class Model { constructor(init: ModelInit) { // we create a base instance first so we can distinguish which fields were explicitly // set by customer code versus those set by normalization. only those fields @@ -822,10 +827,12 @@ const createModelClass = ( const modelInstanceMetadata: ModelInstanceMetadata = isInternallyInitialized - ? (init) - : {}; + ? (init as unknown as ModelInstanceMetadata) + : ({} as ModelInstanceMetadata); - type ModelWithIDIdentifier = { id: string }; + interface ModelWithIDIdentifier { + id: string; + } const { id: _id } = modelInstanceMetadata as unknown as ModelWithIDIdentifier; @@ -839,10 +846,10 @@ const createModelClass = ( ? amplifyUuid() : ulid(); - ((draft)).id = id; + (draft as unknown as ModelWithIDIdentifier).id = id; } else if (isIdOptionallyManaged(modelDefinition)) { // only auto-populate if the id was not provided - ((draft)).id = + (draft as unknown as ModelWithIDIdentifier).id = draft.id || amplifyUuid(); } @@ -868,8 +875,9 @@ const createModelClass = ( // "cloud managed" fields, like createdAt and updatedAt.) const normalized = produce( baseInstance, - (draft: Draft) => - normalize(modelDefinition, draft), + (draft: Draft) => { + normalize(modelDefinition, draft); + }, ); initPatches.set(normalized, patches); @@ -889,7 +897,7 @@ const createModelClass = ( const model = produce( source, draft => { - fn(>draft); + fn(draft as MutableModel); const keyNames = extractPrimaryKeyFieldNames(modelDefinition); // Keys are immutable @@ -900,7 +908,7 @@ const createModelClass = ( { source }, ); } - (draft as Object)[key] = source[key]; + (draft as object)[key] = source[key]; }); const modelValidator = validateModelFields(modelDefinition); @@ -962,7 +970,7 @@ const createModelClass = ( return attached(instance, ModelAttachment.DataStore); } - }); + } as unknown as PersistentModelConstructor; clazz[immerable] = true; @@ -977,7 +985,7 @@ const createModelClass = ( pkField: extractPrimaryKeyFieldNames(modelDefinition), }); for (const relationship of allModelRelationships) { - const field = relationship.field; + const { field } = relationship; Object.defineProperty(clazz.prototype, modelDefinition.fields[field].name, { set(model: T | undefined | null) { @@ -989,7 +997,7 @@ const createModelClass = ( // Avoid validation error when processing AppSync response with nested // selection set. Nested entitites lack version field and can not be validated // TODO: explore a more reliable method to solve this - if (model.hasOwnProperty('_version')) { + if (Object.prototype.hasOwnProperty.call(model, '_version')) { const modelConstructor = Object.getPrototypeOf(model || {}) .constructor as PersistentModelConstructor; @@ -1035,7 +1043,7 @@ const createModelClass = ( // if the memos already has a result for this field, we'll use it. // there is no "cache" invalidation of any kind; memos are permanent to // keep an immutable perception of the instance. - if (!instanceMemos.hasOwnProperty(field)) { + if (!Object.prototype.hasOwnProperty.call(instanceMemos, field)) { // before we populate the memo, we need to know where to look for relatives. // today, this only supports DataStore. Models aren't managed elsewhere in Amplify. if (getAttachment(this) === ModelAttachment.DataStore) { @@ -1047,12 +1055,14 @@ const createModelClass = ( relationship.remoteModelConstructor as PersistentModelConstructor, base => base.and(q => { - return relationship.remoteJoinFields.map((field, index) => { - // TODO: anything we can use instead of `any` here? - return (q[field] as T[typeof field]).eq( - this[relationship.localJoinFields[index]], - ); - }); + return relationship.remoteJoinFields.map( + (joinField, index) => { + // TODO: anything we can use instead of `any` here? + return (q[joinField] as T[typeof joinField]).eq( + this[relationship.localJoinFields[index]], + ); + }, + ); }), ); @@ -1109,9 +1119,9 @@ export class AsyncItem extends Promise {} * This collection can be async-iterated or turned directly into an array using `toArray()`. */ export class AsyncCollection implements AsyncIterable { - private values: Array | Promise>; + private values: any[] | Promise; - constructor(values: Array | Promise>) { + constructor(values: any[] | Promise) { this.values = values; } @@ -1129,6 +1139,7 @@ export class AsyncCollection implements AsyncIterable { [Symbol.asyncIterator](): AsyncIterator { let values; let index = 0; + return { next: async () => { if (!values) values = await this.values; @@ -1138,8 +1149,10 @@ export class AsyncCollection implements AsyncIterable { done: false, }; index++; + return result; } + return { value: null, done: true, @@ -1169,6 +1182,7 @@ export class AsyncCollection implements AsyncIterable { break; } } + return output; } } @@ -1206,7 +1220,7 @@ const checkReadOnlyPropertyOnUpdate = ( const createNonModelClass = ( typeDefinition: SchemaNonModel, ) => { - const clazz = >(class Model { + const clazz = class Model { constructor(init: ModelInit) { const instance = produce( this, @@ -1217,7 +1231,7 @@ const createNonModelClass = ( return instance; } - }); + } as unknown as NonModelTypeConstructor; clazz[immerable] = true; @@ -1235,6 +1249,7 @@ function isQueryOne(obj: any): obj is string { function defaultConflictHandler(conflictData: SyncConflict): PersistentModel { const { localModel, modelConstructor, remoteModel } = conflictData; const { _version } = remoteModel; + return modelInstanceCreator(modelConstructor, { ...localModel, _version }); } @@ -1291,14 +1306,14 @@ async function checkSchemaVersion( storage: Storage, version: string, ): Promise { - const Setting = + const SettingCtor = dataStoreClasses.Setting as PersistentModelConstructor; const modelDefinition = schema.namespaces[DATASTORE].models.Setting; await storage.runExclusive(async s => { const [schemaVersionSetting] = await s.query( - Setting, + SettingCtor, ModelPredicateCreator.createFromAST(modelDefinition, { and: { key: { eq: SETTING_SCHEMA_VERSION } }, }), @@ -1316,7 +1331,7 @@ async function checkSchemaVersion( } } else { await s.save( - modelInstanceCreator(Setting, { + modelInstanceCreator(SettingCtor, { key: SETTING_SCHEMA_VERSION, value: JSON.stringify(version), }), @@ -1394,8 +1409,8 @@ class DataStore { private errorHandler!: (error: SyncError) => void; private fullSyncInterval!: number; private initialized?: Promise; - private initReject!: Function; - private initResolve!: Function; + private initReject!: () => void; + private initResolve!: () => void; private maxRecordsToSync!: number; private storage?: Storage; private sync?: SyncEngine; @@ -1403,12 +1418,14 @@ class DataStore { private syncExpressions!: SyncExpression[]; private syncPredicates: WeakMap | null> = new WeakMap>(); + private sessionId?: string; private storageAdapter!: Adapter; // object that gets passed to descendent classes. Allows us to pass these down by reference private amplifyContext: AmplifyContext = { InternalAPI: this.InternalAPI, }; + private connectivityMonitor?: DataStoreConnectivity; /** @@ -1501,12 +1518,13 @@ class DataStore { this.state = DataStoreState.Starting; if (this.initialized === undefined) { logger.debug('Starting DataStore'); - this.initialized = new Promise((res, rej) => { - this.initResolve = res; - this.initReject = rej; + this.initialized = new Promise((resolve, reject) => { + this.initResolve = resolve; + this.initReject = reject; }); } else { await this.initialized; + return; } @@ -1629,7 +1647,7 @@ class DataStore { throw new Error('No storage to query'); } - //#region Input validation + // #region Input validation if (!isValidModelConstructor(modelConstructor)) { const msg = 'Constructor is not for a valid model'; @@ -1675,10 +1693,10 @@ class DataStore { ); } else { // Object is being queried using object literal syntax - if (isIdentifierObject(identifierOrCriteria, modelDefinition)) { + if (isIdentifierObject(identifierOrCriteria as T, modelDefinition)) { const predicate = ModelPredicateCreator.createForPk( modelDefinition, - identifierOrCriteria, + identifierOrCriteria as T, ); result = await this.storage.query( modelConstructor, @@ -1710,7 +1728,7 @@ class DataStore { } } - //#endregion + // #endregion const returnOne = isQueryOne(identifierOrCriteria) || @@ -1757,7 +1775,9 @@ class DataStore { | undefined = updatedPatchesTuple || initPatchesTuple; const modelConstructor: PersistentModelConstructor | undefined = - model ? >model.constructor : undefined; + model + ? (model.constructor as PersistentModelConstructor) + : undefined; if (!isValidModelConstructor(modelConstructor)) { const msg = 'Object is not an instance of a valid model'; @@ -1816,12 +1836,8 @@ class DataStore { : undefined; const [savedModel] = await this.storage.runExclusive(async s => { - const saved = await s.save( - model, - producedCondition, - undefined, - patchesTuple, - ); + await s.save(model, producedCondition, undefined, patchesTuple); + return s.query( modelConstructor, ModelPredicateCreator.createForPk(modelDefinition, model), @@ -1942,7 +1958,7 @@ class DataStore { if (isIdentifierObject(identifierOrCriteria, modelDefinition)) { condition = ModelPredicateCreator.createForPk( modelDefinition, - identifierOrCriteria, + identifierOrCriteria as T, ); } else { condition = internals( @@ -2057,11 +2073,11 @@ class DataStore { : undefined; if (modelOrConstructor && modelConstructor === undefined) { - const model = modelOrConstructor; - const modelConstructor = - model && (Object.getPrototypeOf(model)).constructor; + const model = modelOrConstructor as T; + const resolvedModelConstructor = + model && (Object.getPrototypeOf(model) as object).constructor; - if (isValidModelConstructor(modelConstructor)) { + if (isValidModelConstructor(resolvedModelConstructor)) { if (identifierOrCriteria) { logger.warn('idOrCriteria is ignored when using a model instance', { model, @@ -2069,7 +2085,7 @@ class DataStore { }); } - return this.observe(modelConstructor, model.id); + return this.observe(resolvedModelConstructor, model.id); } else { const msg = 'The model is not an instance of a PersistentModelConstructor'; @@ -2169,8 +2185,12 @@ class DataStore { observer.next(message as SubscriptionMessage); } }, 'datastore observe message handler'), - error: err => observer.error(err), - complete: () => observer.complete(), + error: err => { + observer.error(err); + }, + complete: () => { + observer.complete(); + }, }); }, 'datastore observe observable initialization') .catch(this.handleAddProcError('DataStore.observe()')) @@ -2189,13 +2209,11 @@ class DataStore { }); }; - observeQuery: { - ( - modelConstructor: PersistentModelConstructor, - criteria?: RecursiveModelPredicateExtender | typeof PredicateAll, - paginationProducer?: ObserveQueryOptions, - ): Observable>; - } = ( + observeQuery: ( + modelConstructor: PersistentModelConstructor, + criteria?: RecursiveModelPredicateExtender | typeof PredicateAll, + paginationProducer?: ObserveQueryOptions, + ) => Observable> = ( model: PersistentModelConstructor, criteria?: RecursiveModelPredicateExtender | typeof PredicateAll, options?: ObserveQueryOptions, @@ -2264,10 +2282,11 @@ class DataStore { // to have visibility into items that move from in-set to out-of-set. // We need to explicitly remove those items from the existing snapshot. handle = this.observe(model).subscribe( - ({ element, model, opType }) => + ({ element, model: observedModel, opType }) => this.runningProcesses.isOpen && this.runningProcesses.add(async () => { - const itemModelDefinition = getModelDefinition(model)!; + const itemModelDefinition = + getModelDefinition(observedModel)!; const idOrPk = getIdentifierValue( itemModelDefinition, element, @@ -2302,7 +2321,7 @@ class DataStore { } const isSynced = - this.sync?.getModelSyncedStatus(model) ?? false; + this.sync?.getModelSyncedStatus(observedModel) ?? false; const limit = itemsChanged.size - deletedItemIds.length >= @@ -2391,8 +2410,11 @@ class DataStore { * @param itemsToSort A array of model type. */ const sortItems = (itemsToSort: T[]): void => { - const modelDefinition = getModelDefinition(model); - const pagination = this.processPagination(modelDefinition!, options); + const sortingModelDefinition = getModelDefinition(model); + const pagination = this.processPagination( + sortingModelDefinition!, + options, + ); const sortPredicates = ModelSortPredicateCreator.getPredicates( pagination!.sort!, @@ -2438,8 +2460,6 @@ class DataStore { const { DataStore: configDataStore, authModeStrategyType: configAuthModeStrategyType, - conflictHandler: configConflictHandler, - errorHandler: configErrorHandler, maxRecordsToSync: configMaxRecordsToSync, syncPageSize: configSyncPageSize, fullSyncInterval: configFullSyncInterval, @@ -2700,6 +2720,7 @@ class DataStore { ): Promise> { try { const condition = await conditionProducer(); + return condition || conditionProducer; } catch (error) { if (error instanceof TypeError) { @@ -2719,6 +2740,7 @@ class DataStore { `You can only utilize one Sync Expression per model. Subsequent sync expressions for the ${name} model will be ignored.`, ); + return map; } diff --git a/packages/datastore/src/index.ts b/packages/datastore/src/index.ts index 81f67de2dbc..1e721b5e50f 100644 --- a/packages/datastore/src/index.ts +++ b/packages/datastore/src/index.ts @@ -1,10 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { + USER, + isModelConstructor, + isNonModelConstructor, + traverseModel, + validatePredicate, +} from './util'; + export { DataStore, DataStoreClass, initSchema, ModelInstanceCreator, + // eslint-disable-next-line import/export AsyncCollection, AsyncItem, } from './datastore/datastore'; @@ -16,14 +25,6 @@ export { } from './predicates'; export { Adapter as StorageAdapter } from './storage/adapter'; -import { - traverseModel, - validatePredicate, - USER, - isNonModelConstructor, - isModelConstructor, -} from './util'; - export { NAMESPACES } from './util'; export const utils = { @@ -34,4 +35,5 @@ export const utils = { isModelConstructor, }; +// eslint-disable-next-line import/export export * from './types'; diff --git a/packages/datastore/src/predicates/index.ts b/packages/datastore/src/predicates/index.ts index db85998f027..ef547da94f3 100644 --- a/packages/datastore/src/predicates/index.ts +++ b/packages/datastore/src/predicates/index.ts @@ -39,6 +39,7 @@ const groupKeys = new Set(['and', 'or', 'not']); */ const isGroup = o => { const keys = [...Object.keys(o)]; + return keys.length === 1 && groupKeys.has(keys[0]); }; @@ -77,6 +78,7 @@ export const comparisonKeys = new Set([ */ const isComparison = o => { const keys = [...Object.keys(o)]; + return !Array.isArray(o) && keys.length === 1 && comparisonKeys.has(keys[0]); }; @@ -98,11 +100,12 @@ export const PredicateAll = Symbol('A predicate that matches all records'); export class Predicates { public static get ALL(): typeof PredicateAll { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const predicate = >(c => c); predicatesAllSet.add(predicate); - return (predicate); + return predicate as unknown as typeof PredicateAll; } } @@ -140,7 +143,7 @@ export class ModelPredicateCreator { */ static getPredicates( predicate: ModelPredicate, - throwOnInvalid: boolean = true, + throwOnInvalid = true, ) { if (throwOnInvalid && !ModelPredicateCreator.isValidPredicate(predicate)) { throw new Error('The predicate is not valid'); @@ -167,6 +170,7 @@ export class ModelPredicateCreator { const predicate = this.createFromAST(modelDefinition, { and: keyFields.map((field, idx) => { const operand = keyValues[idx]; + return { [field]: { eq: operand } }; }), }); @@ -190,6 +194,7 @@ export class ModelPredicateCreator { const ast = { and: Object.entries(flatEqualities).map(([k, v]) => ({ [k]: { eq: v } })), }; + return this.createFromAST(modelDefinition, ast); } @@ -231,12 +236,14 @@ export class ModelPredicateCreator { const children = this.transformGraphQLFilterNodeToPredicateAST( gql[groupkey], ); + return { type: groupkey, predicates: Array.isArray(children) ? children : [children], }; } else if (isComparison(gql)) { const operatorKey = Object.keys(gql)[0]; + return { operator: operatorKey, operand: gql[operatorKey], @@ -246,6 +253,7 @@ export class ModelPredicateCreator { return gql.map(o => this.transformGraphQLFilterNodeToPredicateAST(o)); } else { const fieldKey = Object.keys(gql)[0]; + return { field: fieldKey, ...this.transformGraphQLFilterNodeToPredicateAST(gql[fieldKey]), diff --git a/packages/datastore/src/predicates/next.ts b/packages/datastore/src/predicates/next.ts index 08362dc9df6..78e44763fda 100644 --- a/packages/datastore/src/predicates/next.ts +++ b/packages/datastore/src/predicates/next.ts @@ -1,38 +1,38 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { - PersistentModel, + AllFieldOperators, ModelFieldType, ModelMeta, - ModelPredicate as StoragePredicate, - AllFieldOperators, - PredicateInternalsKey, V5ModelPredicate as ModelPredicate, + PersistentModel, + PredicateInternalsKey, RecursiveModelPredicate, - RecursiveModelPredicateExtender, RecursiveModelPredicateAggregateExtender, + RecursiveModelPredicateExtender, + ModelPredicate as StoragePredicate, } from '../types'; +import { ExclusiveStorage as StorageAdapter } from '../storage/storage'; +import { ModelRelationship } from '../storage/relationship'; +import { asyncEvery, asyncSome } from '../util'; import { ModelPredicateCreator as FlatModelPredicateCreator, comparisonKeys, } from './index'; -import { ExclusiveStorage as StorageAdapter } from '../storage/storage'; -import { ModelRelationship } from '../storage/relationship'; -import { asyncSome, asyncEvery } from '../util'; const ops = [...comparisonKeys] as AllFieldOperators[]; type GroupOperator = 'and' | 'or' | 'not'; -type UntypedCondition = { - fetch: (storage: StorageAdapter) => Promise[]>; - matches: (item: Record) => Promise; +interface UntypedCondition { + fetch(storage: StorageAdapter): Promise[]>; + matches(item: Record): Promise; copy( extract?: GroupCondition, ): [UntypedCondition, GroupCondition | undefined]; toAST(): any; -}; +} /** * A map from keys (exposed to customers) to the internal predicate data @@ -52,6 +52,7 @@ const predicateInternalsMap = new Map(); const registerPredicateInternals = (condition: GroupCondition, key?: any) => { const finalKey = key || new PredicateInternalsKey(); predicateInternalsMap.set(finalKey, condition); + return finalKey; }; @@ -72,6 +73,7 @@ export const internals = (key: any) => { "Invalid predicate. Terminate your predicate with a valid condition (e.g., `p => p.field.eq('value')`) or pass `Predicates.ALL`.", ); } + return predicateInternalsMap.get(key)!; }; @@ -113,7 +115,7 @@ export class FieldCondition { * @param extract Not used. Present only to fulfill the `UntypedCondition` interface. * @returns A new, identitical `FieldCondition`. */ - copy(extract?: GroupCondition): [FieldCondition, GroupCondition | undefined] { + copy(): [FieldCondition, GroupCondition | undefined] { return [ new FieldCondition(this.field, this.operator, [...this.operands]), undefined, @@ -191,7 +193,8 @@ export class FieldCondition { * @param storage N/A. If ever implemented, the storage adapter to query. * @returns N/A. If ever implemented, return items from `storage` that match. */ - async fetch(storage: StorageAdapter): Promise[]> { + async fetch(): Promise[]> { + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject('No implementation needed [yet].'); } @@ -217,6 +220,7 @@ export class FieldCondition { const operation = operations[this.operator as keyof typeof operations]; if (operation) { const result = operation(); + return result; } else { throw new Error(`Invalid operator given: ${this.operator}`); @@ -234,6 +238,7 @@ export class FieldCondition { */ const argumentCount = count => { const argsClause = count === 1 ? 'argument is' : 'arguments are'; + return () => { if (this.operands.length !== count) { return `Exactly ${count} ${argsClause} required.`; @@ -278,6 +283,7 @@ export class FieldCondition { */ const getGroupId = (() => { let seed = 1; + return () => `group_${seed++}`; })(); @@ -345,7 +351,7 @@ export class GroupCondition { * This is used to guard against infinitely fetch -> optimize -> fetch * recursion. */ - public isOptimized: boolean = false, + public isOptimized = false, ) {} /** @@ -386,6 +392,7 @@ export class GroupCondition { */ withFieldConditionsOnly(negate: boolean) { const negateChildren = negate !== (this.operator === 'not'); + return new GroupCondition( this.model, undefined, @@ -495,7 +502,7 @@ export class GroupCondition { return this.optimized().fetch(storage); } - const resultGroups: Array[]> = []; + const resultGroups: Record[][] = []; const operator = (negate ? negations[this.operator] : this.operator) as | 'or' @@ -564,7 +571,7 @@ export class GroupCondition { const relationship = ModelRelationship.from(this.model, g.field); - type JoinCondition = { [x: string]: { eq: any } }; + type JoinCondition = Record; if (relationship) { const allJoinConditions: { and: JoinCondition[] }[] = []; for (const relative of relatives) { @@ -665,7 +672,7 @@ export class GroupCondition { */ async matches( item: Record, - ignoreFieldName: boolean = false, + ignoreFieldName = false, ): Promise { const itemToCheck = this.field && !ignoreFieldName ? await item[this.field] : item; @@ -686,6 +693,7 @@ export class GroupCondition { return true; } } + return false; } @@ -699,6 +707,7 @@ export class GroupCondition { 'Invalid arguments! `not()` accepts exactly one predicate expression.', ); } + return !(await this.operands[0].matches(itemToCheck)); } else { throw new Error('Invalid group operator!'); @@ -769,7 +778,7 @@ export class GroupCondition { */ export function recursivePredicateFor( ModelType: ModelMeta, - allowRecursion: boolean = true, + allowRecursion = true, field?: string, query?: GroupCondition, tail?: GroupCondition, @@ -788,15 +797,16 @@ export function recursivePredicateFor( registerPredicateInternals(baseCondition, link); const copyLink = () => { - const [query, newTail] = baseCondition.copy(tailCondition); + const [copiedQuery, newTail] = baseCondition.copy(tailCondition); const newLink = recursivePredicateFor( ModelType, allowRecursion, undefined, - query, + copiedQuery, newTail, ); - return { query, newTail, newLink }; + + return { query: copiedQuery, newTail, newLink }; }; // Adds .or() and .and() methods to the link. @@ -805,7 +815,7 @@ export function recursivePredicateFor( link[op] = (builder: RecursiveModelPredicateAggregateExtender) => { // or() and and() will return a copy of the original link // to head off mutability concerns. - const { query, newTail } = copyLink(); + const { query: copiedLinkQuery, newTail } = copyLink(); const childConditions = builder( recursivePredicateFor(ModelType, allowRecursion), @@ -829,7 +839,7 @@ export function recursivePredicateFor( ); // FinalPredicate - return registerPredicateInternals(query); + return registerPredicateInternals(copiedLinkQuery); }; }); @@ -839,7 +849,7 @@ export function recursivePredicateFor( ): PredicateInternalsKey => { // not() will return a copy of the original link // to head off mutability concerns. - const { query, newTail } = copyLink(); + const { query: copiedLinkQuery, newTail } = copyLink(); // unlike and() and or(), the customer will supply a "singular" child predicate. // the difference being: not() does not accept an array of predicate-like objects. @@ -853,7 +863,7 @@ export function recursivePredicateFor( // A `FinalModelPredicate`. // Return a thing that can no longer be extended, but instead used to `async filter(items)` // or query storage: `.__query.fetch(storage)`. - return registerPredicateInternals(query); + return registerPredicateInternals(copiedLinkQuery); }; // For each field on the model schema, we want to add a getter @@ -881,7 +891,7 @@ export function recursivePredicateFor( [operator]: (...operands: any[]) => { // build off a fresh copy of the existing `link`, just in case // the same link is being used elsewhere by the customer. - const { query, newTail } = copyLink(); + const { query: copiedLinkQuery, newTail } = copyLink(); // normalize operands. if any of the values are `undefiend`, use // `null` instead, because that's what will be stored cross-platform. @@ -898,7 +908,7 @@ export function recursivePredicateFor( // A `FinalModelPredicate`. // Return a thing that can no longer be extended, but instead used to `async filter(items)` // or query storage: `.__query.fetch(storage)`. - return registerPredicateInternals(query); + return registerPredicateInternals(copiedLinkQuery); }, }; }, {}); @@ -945,6 +955,7 @@ export function recursivePredicateFor( newquery, newtail, ); + return newlink; } else { throw new Error( diff --git a/packages/datastore/src/predicates/sort.ts b/packages/datastore/src/predicates/sort.ts index 99f0e47b077..aadb5146cf0 100644 --- a/packages/datastore/src/predicates/sort.ts +++ b/packages/datastore/src/predicates/sort.ts @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { PersistentModel, - SchemaModel, - SortPredicate, ProducerSortPredicate, + SchemaModel, SortDirection, + SortPredicate, SortPredicatesGroup, } from '../types'; @@ -21,32 +21,29 @@ export class ModelSortPredicateCreator { const { name: modelName } = modelDefinition; const fieldNames = new Set(Object.keys(modelDefinition.fields)); - let handler: ProxyHandler>; - const predicate = new Proxy( - {} as SortPredicate, - (handler = { - get(_target, propertyKey, receiver: SortPredicate) { - const field = propertyKey as keyof T; + const predicate = new Proxy({} as SortPredicate, { + get(_target, propertyKey, receiver: SortPredicate) { + const field = propertyKey as keyof T; - if (!fieldNames.has(field)) { - throw new Error( - `Invalid field for model. field: ${String( - field, - )}, model: ${modelName}`, - ); - } + if (!fieldNames.has(field)) { + throw new Error( + `Invalid field for model. field: ${String( + field, + )}, model: ${modelName}`, + ); + } - const result = (sortDirection: SortDirection) => { - ModelSortPredicateCreator.sortPredicateGroupsMap - .get(receiver) - ?.push({ field, sortDirection }); + const result = (sortDirection: SortDirection) => { + ModelSortPredicateCreator.sortPredicateGroupsMap + .get(receiver) + ?.push({ field, sortDirection }); - return receiver; - }; - return result; - }, - }), - ); + return receiver; + }; + + return result; + }, + }); ModelSortPredicateCreator.sortPredicateGroupsMap.set(predicate, []); @@ -61,7 +58,7 @@ export class ModelSortPredicateCreator { static getPredicates( predicate: SortPredicate, - throwOnInvalid: boolean = true, + throwOnInvalid = true, ): SortPredicatesGroup { if ( throwOnInvalid && diff --git a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts index 9be4d353755..b903c55dc2f 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import AsyncStorageDatabase from './AsyncStorageDatabase'; import { ModelInstanceMetadata, ModelPredicate, @@ -13,21 +12,27 @@ import { } from '../../types'; import { DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR, - traverseModel, - validatePredicate, + getIndexKeys, + getStorename, inMemoryPagination, keysEqual, - getStorename, - getIndexKeys, + traverseModel, + validatePredicate, } from '../../util'; + +import AsyncStorageDatabase from './AsyncStorageDatabase'; import { StorageAdapterBase } from './StorageAdapterBase'; export class AsyncStorageAdapter extends StorageAdapterBase { protected db!: AsyncStorageDatabase; - // no-ops for this adapter - protected async preSetUpChecks() {} - protected async preOpCheck() {} + protected async preSetUpChecks() { + // no-ops for AsyncStorageAdapter + } + + protected async preOpCheck() { + // no-ops for AsyncStorageAdapter + } /** * Open AsyncStorage database @@ -40,6 +45,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { protected async initDb(): Promise { const db = new AsyncStorageDatabase(); await db.init(); + return db; } @@ -77,15 +83,20 @@ export class AsyncStorageAdapter extends StorageAdapterBase { const keyValuesPath = this.getIndexKeyValuesPath(model); - const { instance } = connectedModels.find(({ instance }) => { - const instanceKeyValuesPath = this.getIndexKeyValuesPath(instance); - return keysEqual([instanceKeyValuesPath], [keyValuesPath]); - })!; + const { instance } = connectedModels.find( + ({ instance: connectedModelInstance }) => { + const instanceKeyValuesPath = this.getIndexKeyValuesPath( + connectedModelInstance, + ); + + return keysEqual([instanceKeyValuesPath], [keyValuesPath]); + }, + )!; batch.push(instance); } - return await this.db.batchSave(storeName, batch, keys); + return this.db.batchSave(storeName, batch, keys); } protected async _get(storeName: string, keyArr: string[]): Promise { @@ -93,7 +104,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR, ); - return await this.db.get(itemKeyValuesPath, storeName); + return (await this.db.get(itemKeyValuesPath, storeName)) as T; } async save( @@ -109,12 +120,15 @@ export class AsyncStorageAdapter extends StorageAdapterBase { const result: [T, OpType.INSERT | OpType.UPDATE][] = []; for await (const resItem of connectionStoreNames) { - const { storeName, item, instance, keys } = resItem; + const { storeName: storeNameForRestItem, item, instance, keys } = resItem; const itemKeyValues: string[] = keys.map(key => item[key]); - const fromDB = await this._get(storeName, itemKeyValues); - const opType: OpType = fromDB ? OpType.UPDATE : OpType.INSERT; + const fromDBForRestItem = (await this._get( + storeNameForRestItem, + itemKeyValues, + )) as T; + const opType: OpType = fromDBForRestItem ? OpType.UPDATE : OpType.INSERT; if ( keysEqual(itemKeyValues, modelKeyValues) || @@ -122,7 +136,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { ) { await this.db.save( item, - storeName, + storeNameForRestItem, keys, itemKeyValues.join(DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR), ); @@ -130,6 +144,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { result.push([instance, opType]); } } + return result; } @@ -151,36 +166,39 @@ export class AsyncStorageAdapter extends StorageAdapterBase { if (queryByKey) { const keyValues = queryByKey.join(DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR); const record = await this.getByKey(storeName, keyValues); + return record ? [record] : []; } if (predicates) { const filtered = await this.filterOnPredicate(storeName, predicates); + return this.inMemoryPagination(filtered, pagination); } if (hasSort || hasPagination) { const all = await this.getAll(storeName); + return this.inMemoryPagination(all, pagination); } return this.getAll(storeName); })()) as T[]; - return await this.load(namespaceName, modelConstructor.name, records); + return this.load(namespaceName, modelConstructor.name, records); } private async getByKey( storeName: string, keyValuePath: string, ): Promise { - return await this.db.get(keyValuePath, storeName); + return (await this.db.get(keyValuePath, storeName)) as T; } private async getAll( storeName: string, ): Promise { - return await this.db.getAll(storeName); + return this.db.getAll(storeName); } private async filterOnPredicate( @@ -189,7 +207,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { ) { const { predicates: predicateObjs, type } = predicates; - const all = await this.getAll(storeName); + const all = (await this.getAll(storeName)) as T[]; const filtered = predicateObjs ? all.filter(m => validatePredicate(m, type, predicateObjs)) @@ -210,7 +228,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { firstOrLast: QueryOne = QueryOne.FIRST, ): Promise { const storeName = this.getStorenameForModel(modelConstructor); - const result = await this.db.getOne(firstOrLast, storeName); + const result = (await this.db.getOne(firstOrLast, storeName)) as T; return result && this.modelInstanceCreator(modelConstructor, result); } @@ -232,7 +250,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { } } - //#region platform-specific helper methods + // #region platform-specific helper methods /** * Retrieves concatenated primary key values from a model @@ -246,7 +264,7 @@ export class AsyncStorageAdapter extends StorageAdapterBase { ); } - //#endregion + // #endregion } export default new AsyncStorageAdapter(); diff --git a/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts b/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts index b78c81c3658..f7592f53640 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ULID } from 'ulid'; + import { ModelInstanceMetadata, OpType, @@ -13,6 +14,7 @@ import { indexNameFromKeys, monotonicUlidFactory, } from '../../util'; + import { createInMemoryStore } from './InMemoryStore'; const DB_NAME = '@AmplifyDatastore'; @@ -72,12 +74,12 @@ class AsyncStorageDatabase { if (id === undefined) { // It is an old entry (without ulid). Need to migrate to new key format - const id = ulidOrId; + const resolvedId = ulidOrId; const newUlid = this.getMonotonicFactory(storeName)(); - const oldKey = this.getLegacyKeyForItem(storeName, id); - const newKey = this.getKeyForItem(storeName, id, newUlid); + const oldKey = this.getLegacyKeyForItem(storeName, resolvedId); + const newKey = this.getKeyForItem(storeName, resolvedId, newUlid); const item = await this.storage.getItem(oldKey); @@ -161,7 +163,7 @@ class AsyncStorageDatabase { ); allItemsKeys.push(key); - itemsMap[key] = { ulid, model: (item) }; + itemsMap[key] = { ulid, model: item as unknown as T }; if (_deleted) { keysToDelete.add(key); @@ -180,6 +182,7 @@ class AsyncStorageDatabase { await new Promise((resolve, reject) => { if (keysToDelete.size === 0) { resolve(); + return; } @@ -208,6 +211,7 @@ class AsyncStorageDatabase { await new Promise((resolve, reject) => { if (keysToSave.size === 0) { resolve(); + return; } @@ -258,6 +262,7 @@ class AsyncStorageDatabase { const itemKey = this.getKeyForItem(storeName, keyValuePath, ulid); const recordAsString = await this.storage.getItem(itemKey); const record = recordAsString && JSON.parse(recordAsString); + return record; } @@ -267,14 +272,17 @@ class AsyncStorageDatabase { const [itemId, ulid] = firstOrLast === QueryOne.FIRST ? (() => { - let id: string, ulid: string; - for ([id, ulid] of collection) break; // Get first element of the set - return [id!, ulid!]; + let resolvedId: string, resolvedUlid: string; + // eslint-disable-next-line no-unreachable-loop + for ([resolvedId, resolvedUlid] of collection) break; // Get first element of the set + + return [resolvedId!, resolvedUlid!]; })() : (() => { - let id: string, ulid: string; - for ([id, ulid] of collection); // Get last element of the set - return [id!, ulid!]; + let resolvedId: string, resolvedUlid: string; + for ([resolvedId, resolvedUlid] of collection); // Get last element of the set + + return [resolvedId!, resolvedUlid!]; })(); const itemKey = this.getKeyForItem(storeName, itemId, ulid); diff --git a/packages/datastore/src/storage/adapter/InMemoryStore.ts b/packages/datastore/src/storage/adapter/InMemoryStore.ts index 0ea858f59dc..c275ee0e85e 100644 --- a/packages/datastore/src/storage/adapter/InMemoryStore.ts +++ b/packages/datastore/src/storage/adapter/InMemoryStore.ts @@ -9,7 +9,11 @@ export class InMemoryStore { multiGet = async (keys: string[]) => { return keys.reduce( - (res, k) => (res.push([k, this.db.get(k)!]), res), + (res, k) => { + res.push([k, this.db.get(k)!]); + + return res; + }, [] as [string, string][], ); }; diff --git a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts index d059dd996d8..6e8a6986160 100644 --- a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts +++ b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts @@ -1,9 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import * as idb from 'idb'; +import { ConsoleLogger } from '@aws-amplify/core'; + import { - isPredicateObj, - isPredicateGroup, ModelInstanceMetadata, ModelPredicate, OpType, @@ -13,18 +13,20 @@ import { PredicateObject, PredicatesGroup, QueryOne, + isPredicateGroup, + isPredicateObj, } from '../../types'; import { + getStorename, + inMemoryPagination, isPrivateMode, + isSafariCompatabilityMode, + keysEqual, traverseModel, validatePredicate, - inMemoryPagination, - keysEqual, - getStorename, - isSafariCompatabilityMode, } from '../../util'; + import { StorageAdapterBase } from './StorageAdapterBase'; -import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('DataStore'); @@ -55,7 +57,7 @@ const DB_VERSION = 3; class IndexedDBAdapter extends StorageAdapterBase { protected db!: idb.IDBPDatabase; - private safariCompatabilityMode: boolean = false; + private safariCompatabilityMode = false; // checks are called by StorageAdapterBase class protected async preSetUpChecks() { @@ -77,7 +79,7 @@ class IndexedDBAdapter extends StorageAdapterBase { * @returns IDB Database instance */ protected async initDb(): Promise { - return await idb.openDB(this.dbName, DB_VERSION, { + return idb.openDB(this.dbName, DB_VERSION, { upgrade: async (db, oldVersion, newVersion, txn) => { // create new database if (oldVersion === 0) { @@ -171,8 +173,6 @@ class IndexedDBAdapter extends StorageAdapterBase { txn.abort(); throw error; } - - return; } }, }); @@ -194,7 +194,7 @@ class IndexedDBAdapter extends StorageAdapterBase { const result = await index.get(this.canonicalKeyPath(keyArr)); - return result; + return result as T; } async clear(): Promise { @@ -228,22 +228,25 @@ class IndexedDBAdapter extends StorageAdapterBase { const result: [T, OpType.INSERT | OpType.UPDATE][] = []; for await (const resItem of connectionStoreNames) { - const { storeName, item, instance, keys } = resItem; - const store = tx.objectStore(storeName); + const { storeName: storeNameForRestItem, item, instance, keys } = resItem; + const storeForRestItem = tx.objectStore(storeNameForRestItem); const itemKeyValues: string[] = keys.map(key => item[key]); - const fromDB = await this._get(store, itemKeyValues); - const opType: OpType = fromDB ? OpType.UPDATE : OpType.INSERT; + const fromDBForRestItem = (await this._get( + storeForRestItem, + itemKeyValues, + )) as T; + const opType: OpType = fromDBForRestItem ? OpType.UPDATE : OpType.INSERT; if ( keysEqual(itemKeyValues, modelKeyValues) || opType === OpType.INSERT ) { - const key = await store + const key = await storeForRestItem .index('byPk') .getKey(this.canonicalKeyPath(itemKeyValues)); - await store.put(item, key); + await storeForRestItem.put(item, key); result.push([instance, opType]); } } @@ -281,16 +284,19 @@ class IndexedDBAdapter extends StorageAdapterBase { // if (queryByKey) { const record = await this.getByKey(storeName, queryByKey); + return record ? [record] : []; } if (predicates) { const filtered = await this.filterOnPredicate(storeName, predicates); + return this.inMemoryPagination(filtered, pagination); } if (hasSort) { const all = await this.getAll(storeName); + return this.inMemoryPagination(all, pagination); } @@ -301,7 +307,7 @@ class IndexedDBAdapter extends StorageAdapterBase { return this.getAll(storeName); })()) as T[]; - return await this.load(namespaceName, modelConstructor.name, records); + return this.load(namespaceName, modelConstructor.name, records); } async queryOne( @@ -316,7 +322,7 @@ class IndexedDBAdapter extends StorageAdapterBase { .objectStore(storeName) .openCursor(undefined, firstOrLast === QueryOne.FIRST ? 'next' : 'prev'); - const result = cursor ? cursor.value : undefined; + const result = cursor ? (cursor.value as T) : undefined; return result && this.modelInstanceCreator(modelConstructor, result); } @@ -337,7 +343,7 @@ class IndexedDBAdapter extends StorageAdapterBase { const result: [T, OpType][] = []; const txn = this.db.transaction(storeName, 'readwrite'); - const store = txn.store; + const { store } = txn; for (const item of items) { const model = this.modelInstanceCreator(modelConstructor, item); @@ -358,18 +364,23 @@ class IndexedDBAdapter extends StorageAdapterBase { const key = await index.getKey(this.canonicalKeyPath(keyValues)); if (!_deleted) { - const { instance } = connectedModels.find(({ instance }) => { - const instanceKeyValues = this.getIndexKeyValuesFromModel(instance); - return keysEqual(instanceKeyValues, keyValues); - })!; + const { instance } = connectedModels.find( + ({ instance: connectedModelInstance }) => { + const instanceKeyValues = this.getIndexKeyValuesFromModel( + connectedModelInstance, + ); + + return keysEqual(instanceKeyValues, keyValues); + }, + )!; result.push([ - (instance), + instance as unknown as T, key ? OpType.UPDATE : OpType.INSERT, ]); await store.put(instance, key); } else { - result.push([(item), OpType.DELETE]); + result.push([item as unknown as T, OpType.DELETE]); if (key) { await store.delete(key); @@ -419,14 +430,14 @@ class IndexedDBAdapter extends StorageAdapterBase { } } - //#region platform-specific helper methods + // #region platform-specific helper methods private async checkPrivate() { - const isPrivate = await isPrivateMode().then(isPrivate => { - return isPrivate; - }); + const isPrivate = await isPrivateMode(); if (isPrivate) { logger.error("IndexedDB not supported in this browser's private mode"); + + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject( "IndexedDB not supported in this browser's private mode", ); @@ -455,6 +466,7 @@ class IndexedDBAdapter extends StorageAdapterBase { private getNamespaceAndModelFromStorename(storeName: string) { const [namespaceName, ...modelNameArr] = storeName.split('_'); + return { namespaceName, modelName: modelNameArr.join('_'), @@ -485,13 +497,13 @@ class IndexedDBAdapter extends StorageAdapterBase { storeName: string, keyValue: string[], ): Promise { - return await this._get(storeName, keyValue); + return (await this._get(storeName, keyValue)) as T; } private async getAll( storeName: string, ): Promise { - return await this.db.getAll(storeName); + return this.db.getAll(storeName); } /** @@ -565,7 +577,7 @@ class IndexedDBAdapter extends StorageAdapterBase { isPredicateGroup(predicateObjs[0]) && (predicateObjs[0] as PredicatesGroup).type !== 'not' ) { - type = (predicateObjs[0] as PredicatesGroup).type; + ({ type } = predicateObjs[0] as PredicatesGroup); predicateObjs = (predicateObjs[0] as PredicatesGroup).predicates; } @@ -702,7 +714,7 @@ class IndexedDBAdapter extends StorageAdapterBase { // nothing intelligent we can do with `not` groups unless or until we start // smashing comparison operators against indexes -- at which point we could // perform some reversal here. - candidateResults = await this.getAll(storeName); + candidateResults = (await this.getAll(storeName)) as T[]; } const filtered = predicateObjs @@ -753,7 +765,7 @@ class IndexedDBAdapter extends StorageAdapterBase { result = pageResults; } else { - result = await this.db.getAll(storeName); + result = (await this.db.getAll(storeName)) as T[]; } return result; @@ -771,9 +783,10 @@ class IndexedDBAdapter extends StorageAdapterBase { if (this.safariCompatabilityMode) { return keyArr.length > 1 ? keyArr : keyArr[0]; } + return keyArr; }; - //#endregion + // #endregion } export default new IndexedDBAdapter(); diff --git a/packages/datastore/src/storage/adapter/StorageAdapterBase.ts b/packages/datastore/src/storage/adapter/StorageAdapterBase.ts index 8936979f79f..cf77ea75242 100644 --- a/packages/datastore/src/storage/adapter/StorageAdapterBase.ts +++ b/packages/datastore/src/storage/adapter/StorageAdapterBase.ts @@ -1,11 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Adapter } from './index'; +import type { IDBPDatabase, IDBPObjectStore } from 'idb'; +import { ConsoleLogger } from '@aws-amplify/core'; + import { ModelInstanceCreator } from '../../datastore/datastore'; import { ModelPredicateCreator } from '../../predicates'; import { InternalSchema, - isPredicateObj, ModelInstanceMetadata, ModelPredicate, NamespaceResolver, @@ -16,21 +17,23 @@ import { PredicateObject, PredicatesGroup, QueryOne, + isPredicateObj, } from '../../types'; import { NAMESPACES, - getStorename, - getIndexKeys, + extractPrimaryKeyFieldNames, extractPrimaryKeyValues, + getIndexKeys, + getStorename, + isModelConstructor, traverseModel, validatePredicate, - isModelConstructor, - extractPrimaryKeyFieldNames, } from '../../util'; -import type { IDBPDatabase, IDBPObjectStore } from 'idb'; -import type AsyncStorageDatabase from './AsyncStorageDatabase'; import { ModelRelationship } from '../relationship'; -import { ConsoleLogger } from '@aws-amplify/core'; + +import type AsyncStorageDatabase from './AsyncStorageDatabase'; + +import { Adapter } from './index'; const logger = new ConsoleLogger('DataStore'); const DB_NAME = 'amplify-datastore'; @@ -46,6 +49,7 @@ export abstract class StorageAdapterBase implements Adapter { namsespaceName: NAMESPACES, modelName: string, ) => PersistentModelConstructor; + protected initPromise!: Promise; protected resolve!: (value?: any) => void; protected reject!: (value?: any) => void; @@ -78,12 +82,13 @@ export abstract class StorageAdapterBase implements Adapter { await this.preSetUpChecks(); if (!this.initPromise) { - this.initPromise = new Promise((res, rej) => { - this.resolve = res; - this.reject = rej; + this.initPromise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; }); } else { await this.initPromise; + return; } if (sessionId) { @@ -195,13 +200,14 @@ export abstract class StorageAdapterBase implements Adapter { const set = new Set(); const connectionStoreNames = Object.values(connectedModels).map( ({ modelName, item, instance }) => { - const storeName = getStorename(namespaceName, modelName); - set.add(storeName); + const resolvedStoreName = getStorename(namespaceName, modelName); + set.add(resolvedStoreName); const keys = getIndexKeys( this.schema.namespaces[namespaceName], modelName, ); - return { storeName, item, instance, keys }; + + return { storeName: resolvedStoreName, item, instance, keys }; }, ); @@ -397,7 +403,7 @@ export abstract class StorageAdapterBase implements Adapter { const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), - [], + [] as T[], ); return [models, deletedModels]; @@ -413,7 +419,7 @@ export abstract class StorageAdapterBase implements Adapter { const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), - [], + [] as T[], ); return [models, deletedModels]; @@ -471,7 +477,7 @@ export abstract class StorageAdapterBase implements Adapter { const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), - [], + [] as T[], ); return [[model], deletedModels]; diff --git a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.native.ts b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.native.ts index 95de512f4bd..f4313eee9a2 100644 --- a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.native.ts +++ b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.native.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { Adapter } from '..'; +// eslint-disable-next-line import/no-named-as-default import AsyncStorageAdapter from '../AsyncStorageAdapter'; const getDefaultAdapter: () => Adapter = () => { diff --git a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts index d2e93163ba0..d4053fb29fe 100644 --- a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts +++ b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { isBrowser, isWebWorker } from '@aws-amplify/core/internals/utils'; + import { Adapter } from '..'; import IndexedDBAdapter from '../IndexedDBAdapter'; +// eslint-disable-next-line import/no-named-as-default import AsyncStorageAdapter from '../AsyncStorageAdapter'; -import { isWebWorker, isBrowser } from '@aws-amplify/core/internals/utils'; + const getDefaultAdapter: () => Adapter = () => { if ((isBrowser && window.indexedDB) || (isWebWorker() && self.indexedDB)) { return IndexedDBAdapter as Adapter; diff --git a/packages/datastore/src/storage/adapter/index.ts b/packages/datastore/src/storage/adapter/index.ts index 84b47c3baf2..67797f64311 100644 --- a/packages/datastore/src/storage/adapter/index.ts +++ b/packages/datastore/src/storage/adapter/index.ts @@ -17,10 +17,10 @@ export interface Adapter extends SystemComponent { model: T, condition?: ModelPredicate, ): Promise<[T, OpType.INSERT | OpType.UPDATE][]>; - delete: ( + delete( modelOrModelConstructor: T | PersistentModelConstructor, condition?: ModelPredicate, - ) => Promise<[T[], T[]]>; + ): Promise<[T[], T[]]>; query( modelConstructor: PersistentModelConstructor, predicate?: ModelPredicate, diff --git a/packages/datastore/src/storage/relationship.ts b/packages/datastore/src/storage/relationship.ts index 3d35d75f8d4..7cb92dfbf4f 100644 --- a/packages/datastore/src/storage/relationship.ts +++ b/packages/datastore/src/storage/relationship.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { isFieldAssociation, ModelFieldType, ModelMeta } from '../types'; +import { ModelFieldType, ModelMeta, isFieldAssociation } from '../types'; /** * Defines a relationship from a LOCAL model.field to a REMOTE model.field and helps @@ -52,6 +52,7 @@ export class ModelRelationship { const relationship = ModelRelationship.from(model, field); relationship && relationships.push(relationship); } + return relationships; } @@ -212,6 +213,7 @@ export class ModelRelationship { // This case is theoretically unnecessary going forward. return [this.explicitRemoteAssociation.targetName!]; } else if (this.explicitRemoteAssociation?.targetNames) { + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain return this.explicitRemoteAssociation?.targetNames!; } else if (this.localAssociatedWith) { return this.localAssociatedWith; @@ -249,6 +251,7 @@ export class ModelRelationship { for (let i = 0; i < this.localJoinFields.length; i++) { fk[this.localJoinFields[i]] = remote[this.remoteJoinFields[i]]; } + return fk; } @@ -278,6 +281,7 @@ export class ModelRelationship { if (localValue === null || localValue === undefined) return null; query[this.remoteJoinFields[i]] = local[this.localJoinFields[i]]; } + return query; } } diff --git a/packages/datastore/src/storage/storage.ts b/packages/datastore/src/storage/storage.ts index 6912df89751..5b8d23747be 100644 --- a/packages/datastore/src/storage/storage.ts +++ b/packages/datastore/src/storage/storage.ts @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Observable, filter, map, Subject } from 'rxjs'; +import { Observable, Subject, filter, map } from 'rxjs'; import { Patch } from 'immer'; +import { Mutex } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; + import { ModelInstanceCreator } from '../datastore/datastore'; import { ModelPredicateCreator } from '../predicates'; import { InternalSchema, + InternalSubscriptionMessage, ModelInstanceMetadata, ModelPredicate, NamespaceResolver, @@ -16,26 +20,24 @@ import { PredicatesGroup, QueryOne, SchemaNamespace, - InternalSubscriptionMessage, SubscriptionMessage, isTargetNameAssociation, } from '../types'; import { - isModelConstructor, + NAMESPACES, STORAGE, + isModelConstructor, validatePredicate, valuesEqual, - NAMESPACES, } from '../util'; import { getIdentifierValue } from '../sync/utils'; + import { Adapter } from './adapter'; import getDefaultAdapter from './adapter/getDefaultAdapter'; -import { Mutex } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; export type StorageSubscriptionMessage = InternalSubscriptionMessage & { - mutator?: Symbol; + mutator?: symbol; }; export type StorageFacade = Omit; @@ -78,6 +80,7 @@ class StorageClass implements StorageFacade { async init() { if (this.initialized !== undefined) { await this.initialized; + return; } logger.debug('Starting Storage'); @@ -85,9 +88,9 @@ class StorageClass implements StorageFacade { let resolve: (value?: void | PromiseLike) => void; let reject: (value?: void | PromiseLike) => void; - this.initialized = new Promise((res, rej) => { - resolve = res; - reject = rej; + this.initialized = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; }); this.adapter!.setUp( @@ -104,7 +107,7 @@ class StorageClass implements StorageFacade { async save( model: T, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, patchesTuple?: [Patch[], PersistentModel], ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { await this.init(); @@ -153,7 +156,7 @@ class StorageClass implements StorageFacade { const element = updateMutationInput || savedElement; - const modelConstructor = (Object.getPrototypeOf(savedElement) as Object) + const modelConstructor = (Object.getPrototypeOf(savedElement) as object) .constructor as PersistentModelConstructor; this.pushStream.next({ @@ -175,17 +178,19 @@ class StorageClass implements StorageFacade { delete( model: T, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]>; + delete( modelConstructor: PersistentModelConstructor, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]>; + async delete( modelOrModelConstructor: T | PersistentModelConstructor, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]> { await this.init(); if (!this.adapter) { @@ -212,6 +217,7 @@ class StorageClass implements StorageFacade { const modelIds = new Set( models.map(model => { const modelId = getIdentifierValue(modelDefinition, model); + return modelId; }), ); @@ -224,7 +230,7 @@ class StorageClass implements StorageFacade { } deleted.forEach(model => { - const modelConstructor = (Object.getPrototypeOf(model) as Object) + const resolvedModelConstructor = (Object.getPrototypeOf(model) as object) .constructor as PersistentModelConstructor; let theCondition: PredicatesGroup | undefined; @@ -237,7 +243,7 @@ class StorageClass implements StorageFacade { } this.pushStream.next({ - model: modelConstructor, + model: resolvedModelConstructor, opType: OpType.DELETE, element: model, mutator, @@ -258,7 +264,7 @@ class StorageClass implements StorageFacade { throw new Error('Storage adapter is missing'); } - return await this.adapter.query(modelConstructor, predicate, pagination); + return this.adapter.query(modelConstructor, predicate, pagination); } async queryOne( @@ -270,13 +276,13 @@ class StorageClass implements StorageFacade { throw new Error('Storage adapter is missing'); } - return await this.adapter.queryOne(modelConstructor, firstOrLast); + return this.adapter.queryOne(modelConstructor, firstOrLast); } observe( modelConstructor?: PersistentModelConstructor | null, predicate?: ModelPredicate | null, - skipOwn?: Symbol, + skipOwn?: symbol, ): Observable> { const listenToAll = !modelConstructor; const { predicates, type } = @@ -331,7 +337,7 @@ class StorageClass implements StorageFacade { async batchSave( modelConstructor: PersistentModelConstructor, items: ModelInstanceMetadata[], - mutator?: Symbol, + mutator?: symbol, ): Promise<[T, OpType][]> { await this.init(); if (!this.adapter) { @@ -367,9 +373,9 @@ class StorageClass implements StorageFacade { const [patches, source] = patchesTuple!; const updatedElement = {}; // extract array of updated fields from patches - const updatedFields = ( - patches.map(patch => patch.path && patch.path[0]) - ); + const updatedFields = patches.map( + patch => patch.path && patch.path[0], + ) as string[]; // check model def for association and replace with targetName if exists const modelConstructor = Object.getPrototypeOf(model) @@ -487,13 +493,13 @@ class ExclusiveStorage implements StorageFacade { } runExclusive(fn: (storage: StorageClass) => Promise) { - return >this.mutex.runExclusive(fn.bind(this, this.storage)); + return this.mutex.runExclusive(fn.bind(this, this.storage)) as Promise; } async save( model: T, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, patchesTuple?: [Patch[], PersistentModel], ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { return this.runExclusive<[T, OpType.INSERT | OpType.UPDATE][]>(storage => @@ -504,17 +510,19 @@ class ExclusiveStorage implements StorageFacade { async delete( model: T, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]>; + async delete( modelConstructor: PersistentModelConstructor, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]>; + async delete( modelOrModelConstructor: T | PersistentModelConstructor, condition?: ModelPredicate, - mutator?: Symbol, + mutator?: symbol, ): Promise<[T[], T[]]> { return this.runExclusive<[T[], T[]]>(storage => { if (isModelConstructor(modelOrModelConstructor)) { @@ -555,7 +563,7 @@ class ExclusiveStorage implements StorageFacade { observe( modelConstructor?: PersistentModelConstructor | null, predicate?: ModelPredicate | null, - skipOwn?: Symbol, + skipOwn?: symbol, ): Observable> { return this.storage.observe(modelConstructor, predicate, skipOwn); } diff --git a/packages/datastore/src/sync/datastoreConnectivity.ts b/packages/datastore/src/sync/datastoreConnectivity.ts index 17ce4bd1b75..10395ed753e 100644 --- a/packages/datastore/src/sync/datastoreConnectivity.ts +++ b/packages/datastore/src/sync/datastoreConnectivity.ts @@ -1,17 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { Observable, Observer, SubscriptionLike } from 'rxjs'; -import { ReachabilityMonitor } from './datastoreReachability'; -import { ConsoleLogger } from '@aws-amplify/core'; -const logger = new ConsoleLogger('DataStore'); +import { ReachabilityMonitor } from './datastoreReachability'; const RECONNECTING_IN = 5000; // 5s this may be configurable in the future -type ConnectionStatus = { +interface ConnectionStatus { // Might add other params in the future online: boolean; -}; +} export default class DataStoreConnectivity { private connectionStatus: ConnectionStatus; @@ -28,6 +26,7 @@ export default class DataStoreConnectivity { if (this.observer) { throw new Error('Subscriber already exists'); } + return new Observable(observer => { this.observer = observer; // Will be used to forward socket connection changes, enhancing Reachability @@ -57,7 +56,6 @@ export default class DataStoreConnectivity { // for consistency with other background processors. async stop() { this.unsubscribe(); - return; } socketDisconnected() { diff --git a/packages/datastore/src/sync/index.ts b/packages/datastore/src/sync/index.ts index bcf1e9c72c3..3575caab2a8 100644 --- a/packages/datastore/src/sync/index.ts +++ b/packages/datastore/src/sync/index.ts @@ -1,37 +1,40 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { BackgroundProcessManager } from '@aws-amplify/core/internals/utils'; -import { Hub, ConsoleLogger } from '@aws-amplify/core'; +import { ConsoleLogger, Hub } from '@aws-amplify/core'; +import { Observable, SubscriptionLike, filter, of } from 'rxjs'; +import { + ConnectionState, + CONNECTION_STATE_CHANGE as PUBSUB_CONNECTION_STATE_CHANGE, + CONTROL_MSG as PUBSUB_CONTROL_MSG, +} from '@aws-amplify/api-graphql'; -import { filter, Observable, of, SubscriptionLike } from 'rxjs'; import { ModelInstanceCreator } from '../datastore/datastore'; import { ModelPredicateCreator } from '../predicates'; import { ExclusiveStorage as Storage } from '../storage/storage'; import { + AmplifyContext, + AuthModeStrategy, ConflictHandler, ControlMessageType, ErrorHandler, InternalSchema, + ManagedIdentifier, ModelInit, ModelInstanceMetadata, + ModelPredicate, MutableModel, NamespaceResolver, OpType, - PersistentModel, + OptionallyManagedIdentifier, PersistentModelConstructor, SchemaModel, SchemaNamespace, TypeConstructorMap, - ModelPredicate, - AuthModeStrategy, - ManagedIdentifier, - OptionallyManagedIdentifier, - AmplifyContext, } from '../types'; -// tslint:disable:no-duplicate-imports import type { __modelMeta__ } from '../types'; +import { SYNC, USER, getNow } from '../util'; -import { getNow, SYNC, USER } from '../util'; import DataStoreConnectivity from './datastoreConnectivity'; import { ModelMerger } from './merger'; import { MutationEventOutbox } from './outbox'; @@ -39,30 +42,25 @@ import { MutationProcessor } from './processors/mutation'; import { CONTROL_MSG, SubscriptionProcessor } from './processors/subscription'; import { SyncProcessor } from './processors/sync'; import { + TransformerMutationType, createMutationInstanceFromModelOperation, getIdentifierValue, predicateToGraphQLCondition, - TransformerMutationType, } from './utils'; -import { - CONTROL_MSG as PUBSUB_CONTROL_MSG, - ConnectionState, - CONNECTION_STATE_CHANGE as PUBSUB_CONNECTION_STATE_CHANGE, -} from '@aws-amplify/api-graphql'; - const logger = new ConsoleLogger('DataStore'); const ownSymbol = Symbol('sync'); -type StartParams = { +interface StartParams { fullSyncInterval: number; -}; +} export declare class MutationEvent { readonly [__modelMeta__]: { identifier: OptionallyManagedIdentifier; }; + public readonly id: string; public readonly model: string; public readonly operation: TransformerMutationType; @@ -80,6 +78,7 @@ export declare class ModelMetadata { readonly [__modelMeta__]: { identifier: ManagedIdentifier; }; + public readonly id: string; public readonly namespace: string; public readonly model: string; @@ -116,15 +115,17 @@ export class SyncEngine { private readonly modelMerger: ModelMerger; private readonly outbox: MutationEventOutbox; private readonly datastoreConnectivity: DataStoreConnectivity; - private readonly modelSyncedStatus: WeakMap< + private readonly modelSyncedStatus = new WeakMap< PersistentModelConstructor, boolean - > = new WeakMap(); + >(); + private unsleepSyncQueriesObservable: (() => void) | null; private waitForSleepState: Promise; private syncQueriesObservableStartSleeping: ( value?: void | PromiseLike, ) => void; + private stopDisruptionListener: () => void; private connectionDisrupted = false; @@ -159,13 +160,12 @@ export class SyncEngine { this.syncQueriesObservableStartSleeping = resolve; }); - const MutationEvent = this.modelClasses[ - 'MutationEvent' - ] as PersistentModelConstructor; + const MutationEventCtor = this.modelClasses + .MutationEvent as PersistentModelConstructor; this.outbox = new MutationEventOutbox( this.schema, - MutationEvent, + MutationEventCtor, modelInstanceCreator, ownSymbol, ); @@ -196,7 +196,7 @@ export class SyncEngine { this.userModelClasses, this.outbox, this.modelInstanceCreator, - MutationEvent, + MutationEventCtor, this.amplifyConfig, this.authModeStrategy, errorHandler, @@ -219,203 +219,200 @@ export class SyncEngine { await this.setupModels(params); } catch (err) { observer.error(err); + return; } // this is awaited at the bottom. so, we don't need to register // this explicitly with the context. it's already contained. - const startPromise = new Promise( - (doneStarting, failedStarting) => { - this.datastoreConnectivity.status().subscribe( - async ({ online }) => - this.runningProcesses.isOpen && - this.runningProcesses.add(async onTerminate => { - // From offline to online - if (online && !this.online) { - this.online = online; - - observer.next({ - type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, - data: { - active: this.online, - }, + const startPromise = new Promise((resolve, reject) => { + const doneStarting = resolve; + const failedStarting = reject; + + this.datastoreConnectivity.status().subscribe( + async ({ online }) => + this.runningProcesses.isOpen && + this.runningProcesses.add(async onTerminate => { + // From offline to online + if (online && !this.online) { + this.online = online; + + observer.next({ + type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, + data: { + active: this.online, + }, + }); + + this.stopDisruptionListener = this.startDisruptionListener(); + // #region GraphQL Subscriptions + const [ctlSubsObservable, dataSubsObservable] = + this.subscriptionsProcessor.start(); + + try { + await new Promise((_resolve, _reject) => { + onTerminate.then(_reject); + const ctlSubsSubscription = ctlSubsObservable.subscribe({ + next: msg => { + if (msg === CONTROL_MSG.CONNECTED) { + _resolve(); + } + }, + error: err => { + _reject(err); + const handleDisconnect = this.disconnectionHandler(); + handleDisconnect(err); + }, + }); + + subscriptions.push(ctlSubsSubscription); }); + } catch (err) { + observer.error(err); + failedStarting(); - let ctlSubsObservable: Observable; - let dataSubsObservable: Observable< - [TransformerMutationType, SchemaModel, PersistentModel] - >; - - this.stopDisruptionListener = - this.startDisruptionListener(); - //#region GraphQL Subscriptions - [ctlSubsObservable, dataSubsObservable] = - this.subscriptionsProcessor.start(); - - try { - await new Promise((resolve, reject) => { - onTerminate.then(reject); - const ctlSubsSubscription = ctlSubsObservable.subscribe( - { - next: msg => { - if (msg === CONTROL_MSG.CONNECTED) { - resolve(); - } - }, - error: err => { - reject(err); - const handleDisconnect = - this.disconnectionHandler(); - handleDisconnect(err); - }, - }, - ); + return; + } - subscriptions.push(ctlSubsSubscription); - }); - } catch (err) { - observer.error(err); - failedStarting(); - return; - } + logger.log('Realtime ready'); - logger.log('Realtime ready'); + observer.next({ + type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED, + }); - observer.next({ - type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED, - }); + // #endregion - //#endregion + // #region Base & Sync queries + try { + await new Promise((_resolve, _reject) => { + const syncQuerySubscription = + this.syncQueriesObservable().subscribe({ + next: message => { + const { type } = message; - //#region Base & Sync queries - try { - await new Promise((resolve, reject) => { - const syncQuerySubscription = - this.syncQueriesObservable().subscribe({ - next: message => { - const { type } = message; + if ( + type === + ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY + ) { + _resolve(); + } - if ( - type === - ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY - ) { - resolve(); - } + observer.next(message); + }, + complete: () => { + _resolve(); + }, + error: error => { + _reject(error); + }, + }); - observer.next(message); - }, - complete: () => { - resolve(); - }, - error: error => { - reject(error); + if (syncQuerySubscription) { + subscriptions.push(syncQuerySubscription); + } + }); + } catch (error) { + observer.error(error); + failedStarting(); + + return; + } + // #endregion + + // #region process mutations (outbox) + subscriptions.push( + this.mutationsProcessor + .start() + .subscribe(({ modelDefinition, model: item, hasMore }) => + this.runningProcesses.add(async () => { + const modelConstructor = this.userModelClasses[ + modelDefinition.name + ] as PersistentModelConstructor; + + const model = this.modelInstanceCreator( + modelConstructor, + item, + ); + + await this.storage.runExclusive(storage => + this.modelMerger.merge( + storage, + model, + modelDefinition, + ), + ); + + observer.next({ + type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED, + data: { + model: modelConstructor, + element: model, }, }); - if (syncQuerySubscription) { - subscriptions.push(syncQuerySubscription); - } - }); - } catch (error) { - observer.error(error); - failedStarting(); - return; - } - //#endregion - - //#region process mutations (outbox) - subscriptions.push( - this.mutationsProcessor - .start() - .subscribe( - ({ modelDefinition, model: item, hasMore }) => - this.runningProcesses.add(async () => { - const modelConstructor = this.userModelClasses[ - modelDefinition.name - ] as PersistentModelConstructor; - - const model = this.modelInstanceCreator( - modelConstructor, - item, - ); - - await this.storage.runExclusive(storage => - this.modelMerger.merge( - storage, - model, - modelDefinition, - ), - ); - - observer.next({ - type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED, - data: { - model: modelConstructor, - element: model, - }, - }); - - observer.next({ - type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS, - data: { - isEmpty: !hasMore, - }, - }); - }, 'mutation processor event'), - ), - ); - //#endregion - - //#region Merge subscriptions buffer - subscriptions.push( - dataSubsObservable!.subscribe( - ([_transformerMutationType, modelDefinition, item]) => - this.runningProcesses.add(async () => { - const modelConstructor = this.userModelClasses[ - modelDefinition.name - ] as PersistentModelConstructor; - - const model = this.modelInstanceCreator( - modelConstructor, - item, - ); - - await this.storage.runExclusive(storage => - this.modelMerger.merge( - storage, - model, - modelDefinition, - ), - ); - }, 'subscription dataSubsObservable event'), + observer.next({ + type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS, + data: { + isEmpty: !hasMore, + }, + }); + }, 'mutation processor event'), ), - ); - //#endregion - } else if (!online) { - this.online = online; - - observer.next({ - type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, - data: { - active: this.online, - }, - }); - - subscriptions.forEach(sub => sub.unsubscribe()); - subscriptions = []; - } + ); + // #endregion + + // #region Merge subscriptions buffer + subscriptions.push( + dataSubsObservable!.subscribe( + ([_transformerMutationType, modelDefinition, item]) => + this.runningProcesses.add(async () => { + const modelConstructor = this.userModelClasses[ + modelDefinition.name + ] as PersistentModelConstructor; + + const model = this.modelInstanceCreator( + modelConstructor, + item, + ); + + await this.storage.runExclusive(storage => + this.modelMerger.merge( + storage, + model, + modelDefinition, + ), + ); + }, 'subscription dataSubsObservable event'), + ), + ); + // #endregion + } else if (!online) { + this.online = online; + + observer.next({ + type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, + data: { + active: this.online, + }, + }); + + subscriptions.forEach(sub => { + sub.unsubscribe(); + }); + subscriptions = []; + } - doneStarting(); - }, 'datastore connectivity event'), - ); - }, - ); + doneStarting(); + }, 'datastore connectivity event'), + ); + }); this.storage .observe(null, null, ownSymbol) .pipe( filter(({ model }) => { const modelDefinition = this.getModelDefinition(model); + return modelDefinition.syncable === true; }), ) @@ -424,9 +421,8 @@ export class SyncEngine { this.runningProcesses.add(async () => { const namespace = this.schema.namespaces[this.namespaceResolver(model)]; - const MutationEventConstructor = this.modelClasses[ - 'MutationEvent' - ] as PersistentModelConstructor; + const MutationEventConstructor = this.modelClasses + .MutationEvent as PersistentModelConstructor; const modelDefinition = this.getModelDefinition(model); const graphQLCondition = predicateToGraphQLCondition( condition!, @@ -494,21 +490,14 @@ export class SyncEngine { private async getModelsMetadataWithNextFullSync( currentTimeStamp: number, ): Promise> { - const modelLastSync: Map = new Map( + const modelLastSync = new Map( ( await this.runningProcesses.add( () => this.getModelsMetadata(), 'sync/index getModelsMetadataWithNextFullSync', ) ).map( - ({ - namespace, - model, - lastSync, - lastFullSync, - fullSyncInterval, - lastSyncPredicate, - }) => { + ({ namespace, model, lastSync, lastFullSync, fullSyncInterval }) => { const nextFullSync = lastFullSync! + fullSyncInterval; const syncFrom = !lastFullSync || nextFullSync < currentTimeStamp @@ -541,14 +530,14 @@ export class SyncEngine { let terminated = false; while (!observer.closed && !terminated) { - const count: WeakMap< + const count = new WeakMap< PersistentModelConstructor, { new: number; updated: number; deleted: number; } - > = new WeakMap(); + >(); const modelLastSync = await this.getModelsMetadataWithNextFullSync( Date.now(), @@ -561,9 +550,11 @@ export class SyncEngine { let start: number; let syncDuration: number; let lastStartedAt: number; - await new Promise((resolve, reject) => { + await new Promise((resolve, _reject) => { if (!this.runningProcesses.isOpen) resolve(); - onTerminate.then(() => resolve()); + onTerminate.then(() => { + resolve(); + }); syncQueriesSubscription = this.syncQueriesProcessor .start(modelLastSync) .subscribe({ @@ -613,6 +604,7 @@ export class SyncEngine { } oneByOne.push(item); + return false; }); @@ -661,7 +653,7 @@ export class SyncEngine { if (done) { const { name: modelName } = modelDefinition; - //#region update last sync for type + // #region update last sync for type let modelMetadata = await this.getModelMetadata( namespace, modelName, @@ -694,7 +686,7 @@ export class SyncEngine { undefined, ownSymbol, ); - //#endregion + // #endregion const counts = count.get(modelConstructor); @@ -768,16 +760,16 @@ export class SyncEngine { // TLDR; this is a lot of complexity here for a sleep(), // but, it's not clear to me yet how to support an // extensible, centralized cancelable `sleep()` elegantly. - await this.runningProcesses.add(async onTerminate => { - let sleepTimer; + await this.runningProcesses.add(async onRunningProcessTerminate => { + let _sleepTimer; let unsleep; - const sleep = new Promise(_unsleep => { - unsleep = _unsleep; - sleepTimer = setTimeout(unsleep, msNextFullSync); + const sleep = new Promise(resolve => { + unsleep = resolve; + _sleepTimer = setTimeout(unsleep, msNextFullSync); }); - onTerminate.then(() => { + onRunningProcessTerminate.then(() => { terminated = true; this.syncQueriesObservableStartSleeping(); unsleep(); @@ -785,6 +777,7 @@ export class SyncEngine { this.unsleepSyncQueriesObservable = unsleep; this.syncQueriesObservableStartSleeping(); + return sleep; }, 'syncQueriesObservable sleep'); @@ -927,10 +920,10 @@ export class SyncEngine { } private async getModelsMetadata(): Promise { - const ModelMetadata = this.modelClasses + const ModelMetadataCtor = this.modelClasses .ModelMetadata as PersistentModelConstructor; - const modelsMetadata = await this.storage.query(ModelMetadata); + const modelsMetadata = await this.storage.query(ModelMetadataCtor); return modelsMetadata; } @@ -939,18 +932,22 @@ export class SyncEngine { namespace: string, model: string, ): Promise { - const ModelMetadata = this.modelClasses + const ModelMetadataCtor = this.modelClasses .ModelMetadata as PersistentModelConstructor; const predicate = ModelPredicateCreator.createFromAST( - this.schema.namespaces[SYNC].models[ModelMetadata.name], + this.schema.namespaces[SYNC].models[ModelMetadataCtor.name], { and: [{ namespace: { eq: namespace } }, { model: { eq: model } }] }, ); - const [modelMetadata] = await this.storage.query(ModelMetadata, predicate, { - page: 0, - limit: 1, - }); + const [modelMetadata] = await this.storage.query( + ModelMetadataCtor, + predicate, + { + page: 0, + limit: 1, + }, + ); return modelMetadata; } @@ -1074,6 +1071,7 @@ export class SyncEngine { }, }, }; + return namespace; } diff --git a/packages/datastore/src/sync/merger.ts b/packages/datastore/src/sync/merger.ts index eaf9d3ecab9..0cd5dde2989 100644 --- a/packages/datastore/src/sync/merger.ts +++ b/packages/datastore/src/sync/merger.ts @@ -7,6 +7,7 @@ import { PersistentModelConstructor, SchemaModel, } from '../types'; + import { MutationEventOutbox } from './outbox'; import { getIdentifierValue } from './utils'; @@ -14,7 +15,7 @@ import { getIdentifierValue } from './utils'; class ModelMerger { constructor( private readonly outbox: MutationEventOutbox, - private readonly ownSymbol: Symbol, + private readonly ownSymbol: symbol, ) {} /** @@ -55,7 +56,7 @@ class ModelMerger { items: ModelInstanceMetadata[], modelDefinition: SchemaModel, ): Promise<[ModelInstanceMetadata, OpType][]> { - const itemsMap: Map = new Map(); + const itemsMap = new Map(); for (const item of items) { // merge items by model id. Latest record for a given id remains. @@ -66,7 +67,7 @@ class ModelMerger { const page = [...itemsMap.values()]; - return await storage.batchSave(modelConstructor, page, this.ownSymbol); + return storage.batchSave(modelConstructor, page, this.ownSymbol); } } diff --git a/packages/datastore/src/sync/outbox.ts b/packages/datastore/src/sync/outbox.ts index d693e79c3f0..b555e47b5dd 100644 --- a/packages/datastore/src/sync/outbox.ts +++ b/packages/datastore/src/sync/outbox.ts @@ -1,11 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { MutationEvent } from './index'; import { ModelPredicateCreator } from '../predicates'; import { ExclusiveStorage as Storage, - StorageFacade, Storage as StorageClass, + StorageFacade, } from '../storage/storage'; import { ModelInstanceCreator } from '../datastore/datastore'; import { @@ -15,8 +14,11 @@ import { QueryOne, SchemaModel, } from '../types'; -import { USER, SYNC, directedValueEquality } from '../util'; -import { getIdentifierValue, TransformerMutationType } from './utils'; +import { SYNC, USER, directedValueEquality } from '../util'; + +import { TransformerMutationType, getIdentifierValue } from './utils'; + +import { MutationEvent } from './index'; // TODO: Persist deleted ids // https://github.com/aws-amplify/amplify-js/blob/datastore-docs/packages/datastore/docs/sync-engine.md#outbox @@ -25,9 +27,9 @@ class MutationEventOutbox { constructor( private readonly schema: InternalSchema, - private readonly MutationEvent: PersistentModelConstructor, + private readonly _MutationEvent: PersistentModelConstructor, private readonly modelInstanceCreator: ModelInstanceCreator, - private readonly ownSymbol: Symbol, + private readonly ownSymbol: symbol, ) {} public async enqueue( @@ -36,7 +38,7 @@ class MutationEventOutbox { ): Promise { await storage.runExclusive(async s => { const mutationEventModelDefinition = - this.schema.namespaces[SYNC].models['MutationEvent']; + this.schema.namespaces[SYNC].models.MutationEvent; // `id` is the key for the record in the mutationEvent; // `modelId` is the key for the actual record that was mutated @@ -51,11 +53,12 @@ class MutationEventOutbox { ); // Check if there are any other records with same id - const [first] = await s.query(this.MutationEvent, predicate); + 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; } @@ -64,7 +67,7 @@ class MutationEventOutbox { if (first.operation === TransformerMutationType.CREATE) { if (incomingMutationType === TransformerMutationType.DELETE) { - await s.delete(this.MutationEvent, predicate); + await s.delete(this._MutationEvent, predicate); } else { // first gets updated with the incoming mutation's data, condition intentionally skipped @@ -72,7 +75,7 @@ class MutationEventOutbox { // data loss, since update mutations only include changed fields const merged = this.mergeUserFields(first, mutationEvent); await s.save( - this.MutationEvent.copyOf(first, draft => { + this._MutationEvent.copyOf(first, draft => { draft.data = merged.data; }), undefined, @@ -89,7 +92,7 @@ class MutationEventOutbox { merged = this.mergeUserFields(first, mutationEvent); // delete all for model - await s.delete(this.MutationEvent, predicate); + await s.delete(this._MutationEvent, predicate); } merged = merged! || mutationEvent; @@ -125,7 +128,7 @@ class MutationEventOutbox { * @param storage */ public async peek(storage: StorageFacade): Promise { - const head = await storage.queryOne(this.MutationEvent, QueryOne.FIRST); + const head = await storage.queryOne(this._MutationEvent, QueryOne.FIRST); this.inProgressMutationEventId = head ? head.id : undefined!; @@ -143,7 +146,7 @@ class MutationEventOutbox { const modelId = getIdentifierValue(userModelDefinition, model); const mutationEvents = await storage.query( - this.MutationEvent, + this._MutationEvent, ModelPredicateCreator.createFromAST(mutationEventModelDefinition, { and: { modelId: { eq: modelId } }, }), @@ -153,7 +156,7 @@ class MutationEventOutbox { } public async getModelIds(storage: StorageFacade): Promise> { - const mutationEvents = await storage.query(this.MutationEvent); + const mutationEvents = await storage.query(this._MutationEvent); const result = new Set(); @@ -205,10 +208,9 @@ class MutationEventOutbox { } const mutationEventModelDefinition = - this.schema.namespaces[SYNC].models['MutationEvent']; + this.schema.namespaces[SYNC].models.MutationEvent; - const userModelDefinition = - this.schema.namespaces['user'].models[head.model]; + const userModelDefinition = this.schema.namespaces.user.models[head.model]; const recordId = getIdentifierValue(userModelDefinition, record); @@ -223,7 +225,7 @@ class MutationEventOutbox { ); const outdatedMutations = await storage.query( - this.MutationEvent, + this._MutationEvent, predicate, ); @@ -236,16 +238,16 @@ class MutationEventOutbox { const newData = { ...oldData, _version, _lastChangedAt }; - return this.MutationEvent.copyOf(m, draft => { + return this._MutationEvent.copyOf(m, draft => { draft.data = JSON.stringify(newData); }); }); - await storage.delete(this.MutationEvent, predicate); + await storage.delete(this._MutationEvent, predicate); await Promise.all( - reconciledMutations.map( - async m => await storage.save(m, undefined, this.ownSymbol), + reconciledMutations.map(async m => + storage.save(m, undefined, this.ownSymbol), ), ); } @@ -273,13 +275,13 @@ class MutationEventOutbox { ...currentData, }); - return this.modelInstanceCreator(this.MutationEvent, { + 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 diff --git a/packages/datastore/src/sync/processors/errorMaps.ts b/packages/datastore/src/sync/processors/errorMaps.ts index b67a0de5cfb..1714c7288b5 100644 --- a/packages/datastore/src/sync/processors/errorMaps.ts +++ b/packages/datastore/src/sync/processors/errorMaps.ts @@ -16,6 +16,7 @@ export const mutationErrorMap: ErrorMap = { BadModel: () => false, BadRecord: error => { const { message } = error; + return ( /^Cannot return \w+ for [\w-_]+ type/.test(message) || /^Variable '.+' has coerced Null value for NonNull type/.test(message) @@ -34,10 +35,12 @@ export const subscriptionErrorMap: ErrorMap = { ConfigError: () => false, Transient: observableError => { const error = unwrapObservableError(observableError); + return connectionTimeout(error) || serverError(error); }, Unauthorized: observableError => { const error = unwrapObservableError(observableError); + return /Connection failed.+Unauthorized/.test(error.message); }, }; @@ -60,6 +63,7 @@ function unwrapObservableError(observableError: any) { const { errors: [error], } = ({ + // eslint-disable-next-line no-empty-pattern errors: [], } = observableError); @@ -92,5 +96,6 @@ export function mapErrorToType(errorMap: ErrorMap, error: Error): ErrorType { return errorType; } } + return 'Unknown'; } diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index 18619d5f251..bc38e26e14e 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -3,61 +3,61 @@ import { GraphQLResult } from '@aws-amplify/api'; import { InternalAPI } from '@aws-amplify/api/internals'; import { + BackgroundProcessManager, Category, CustomUserAgentDetails, DataStoreAction, - jitteredBackoff, + GraphQLAuthMode, NonRetryableError, + jitteredBackoff, retry, - BackgroundProcessManager, - GraphQLAuthMode, - AmplifyError, } from '@aws-amplify/core/internals/utils'; - import { Observable, Observer } from 'rxjs'; +import { ConsoleLogger } from '@aws-amplify/core'; + import { MutationEvent } from '../'; import { ModelInstanceCreator } from '../../datastore/datastore'; import { ExclusiveStorage as Storage } from '../../storage/storage'; import { + AmplifyContext, AuthModeStrategy, ConflictHandler, DISCARD, ErrorHandler, GraphQLCondition, InternalSchema, - isModelFieldType, - isTargetNameAssociation, ModelInstanceMetadata, OpType, PersistentModel, PersistentModelConstructor, + ProcessName, SchemaModel, TypeConstructorMap, - ProcessName, - AmplifyContext, + isModelFieldType, + isTargetNameAssociation, } from '../../types'; -import { extractTargetNamesFromSrc, USER, ID } from '../../util'; +import { ID, USER, extractTargetNamesFromSrc } from '../../util'; import { MutationEventOutbox } from '../outbox'; import { + TransformerMutationType, buildGraphQLOperation, createMutationInstanceFromModelOperation, getModelAuthModes, - TransformerMutationType, getTokenForCustomAuth, } from '../utils'; + import { getMutationErrorType } from './errorMaps'; -import { ConsoleLogger } from '@aws-amplify/core'; const MAX_ATTEMPTS = 10; const logger = new ConsoleLogger('DataStore'); -type MutationProcessorEvent = { +interface MutationProcessorEvent { operation: TransformerMutationType; modelDefinition: SchemaModel; model: PersistentModel; hasMore: boolean; -}; +} class MutationProcessor { /** @@ -73,7 +73,8 @@ class MutationProcessor { SchemaModel, [TransformerMutationType, string, string][] >(); - private processing: boolean = false; + + private processing = false; private runningProcesses = new BackgroundProcessManager(); @@ -83,7 +84,7 @@ class MutationProcessor { private readonly userClasses: TypeConstructorMap, private readonly outbox: MutationEventOutbox, private readonly modelInstanceCreator: ModelInstanceCreator, - private readonly MutationEvent: PersistentModelConstructor, + private readonly _MutationEvent: PersistentModelConstructor, private readonly amplifyConfig: Record = {}, private readonly authModeStrategy: AuthModeStrategy, private readonly errorHandler: ErrorHandler, @@ -216,7 +217,7 @@ class MutationProcessor { data, condition, modelConstructor, - this.MutationEvent, + this._MutationEvent, head, operationAuthModes[authModeAttempts], onTerminate, @@ -236,6 +237,7 @@ class MutationProcessor { }`, ); try { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await 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', @@ -260,7 +262,8 @@ class MutationProcessor { operationAuthModes[authModeAttempts] }`, ); - return await authModeRetry(); + + return authModeRetry(); } }; @@ -313,30 +316,30 @@ class MutationProcessor { data: string, condition: string, modelConstructor: PersistentModelConstructor, - MutationEvent: PersistentModelConstructor, + MutationEventCtor: PersistentModelConstructor, mutationEvent: MutationEvent, authMode: GraphQLAuthMode, onTerminate: Promise, ): Promise< [GraphQLResult>, string, SchemaModel] > { - return await retry( + return retry( async ( - model: string, - operation: TransformerMutationType, - data: string, - condition: string, - modelConstructor: PersistentModelConstructor, - MutationEvent: PersistentModelConstructor, - mutationEvent: MutationEvent, + retriedModel: string, + retriedOperation: TransformerMutationType, + retriedData: string, + retriedCondition: string, + retriedModelConstructor: PersistentModelConstructor, + retiredMutationEventCtor: PersistentModelConstructor, + retiredMutationEvent: MutationEvent, ) => { const [query, variables, graphQLCondition, opName, modelDefinition] = this.createQueryVariables( namespaceName, - model, - operation, - data, - condition, + retriedModel, + retriedOperation, + retriedData, + retriedCondition, ); const authToken = await getTokenForCustomAuth( @@ -352,7 +355,7 @@ class MutationProcessor { }; let attempt = 0; - const opType = this.opTypeFromTransformerOperation(operation); + const opType = this.opTypeFromTransformerOperation(retriedOperation); const customUserAgentDetails: CustomUserAgentDetails = { category: Category.DataStore, @@ -361,13 +364,11 @@ class MutationProcessor { do { try { - const result = >>( - await this.amplifyContext.InternalAPI.graphql( - tryWith, - undefined, - customUserAgentDetails, - ) - ); + const result = (await this.amplifyContext.InternalAPI.graphql( + tryWith, + undefined, + customUserAgentDetails, + )) as GraphQLResult>; // Use `as any` because TypeScript doesn't seem to like passing tuples // through generic params. @@ -402,20 +403,20 @@ class MutationProcessor { } else { try { retryWith = await this.conflictHandler!({ - modelConstructor, + modelConstructor: retriedModelConstructor, localModel: this.modelInstanceCreator( - modelConstructor, + retriedModelConstructor, variables.input, ), remoteModel: this.modelInstanceCreator( - modelConstructor, + retriedModelConstructor, error.data, ), operation: opType, attempts: attempt, }); - } catch (err) { - logger.warn('conflict trycatch', err); + } catch (caughtErr) { + logger.warn('conflict trycatch', caughtErr); continue; } } @@ -423,33 +424,32 @@ class MutationProcessor { if (retryWith === DISCARD) { // Query latest from server and notify merger - const [[, opName, query]] = buildGraphQLOperation( + const [[, builtOpName, builtQuery]] = buildGraphQLOperation( this.schema.namespaces[namespaceName], modelDefinition, 'GET', ); - const authToken = await getTokenForCustomAuth( + const newAuthToken = await getTokenForCustomAuth( authMode, this.amplifyConfig, ); - const serverData = < - GraphQLResult> - >await this.amplifyContext.InternalAPI.graphql( - { - query, - variables: { id: variables.input.id }, - authMode, - authToken, - }, - undefined, - customUserAgentDetails, - ); + const serverData = + (await this.amplifyContext.InternalAPI.graphql( + { + query: builtQuery, + variables: { id: variables.input.id }, + authMode, + authToken: newAuthToken, + }, + undefined, + customUserAgentDetails, + )) as GraphQLResult>; // onTerminate cancel graphql() - return [serverData, opName, modelDefinition]; + return [serverData, builtOpName, modelDefinition]; } const namespace = this.schema.namespaces[namespaceName]; @@ -460,12 +460,12 @@ class MutationProcessor { namespace.relationships!, modelDefinition, opType, - modelConstructor, + retriedModelConstructor, retryWith, graphQLCondition, - MutationEvent, + retiredMutationEventCtor, this.modelInstanceCreator, - mutationEvent.id, + retiredMutationEvent.id, ); await this.storage.save(updatedMutation); @@ -478,19 +478,23 @@ class MutationProcessor { '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: retriedOperation, errorType: getMutationErrorType(error), errorInfo: error.errorInfo, process: ProcessName.mutate, cause: error, remoteModel: error.data - ? this.modelInstanceCreator(modelConstructor, error.data) + ? this.modelInstanceCreator( + retriedModelConstructor, + error.data, + ) : null!, }); - } catch (err) { - logger.warn('Mutation error handler failed with:', err); + } catch (caughtErr) { + logger.warn('Mutation error handler failed with:', caughtErr); } finally { // Return empty tuple, dequeues the mutation + // eslint-disable-next-line no-unsafe-finally return error.data ? [ { data: { [opName]: error.data } }, @@ -506,6 +510,7 @@ class MutationProcessor { throw new NonRetryableError(err); } } + // eslint-disable-next-line no-unmodified-loop-condition } while (tryWith); }, [ @@ -514,7 +519,7 @@ class MutationProcessor { data, condition, modelConstructor, - MutationEvent, + MutationEventCtor, mutationEvent, ], safeJitteredBackoff, @@ -543,7 +548,9 @@ class MutationProcessor { ([transformerMutationType]) => transformerMutationType === operation, )!; - const { _version, ...parsedData } = JSON.parse(data); + const { _version, ...parsedData } = JSON.parse( + data, + ) as ModelInstanceMetadata; // include all the fields that comprise a custom PK if one is specified const deleteInput = {}; @@ -552,14 +559,14 @@ class MutationProcessor { deleteInput[pkField] = parsedData[pkField]; } } else { - deleteInput[ID] = (parsedData).id; + deleteInput[ID] = (parsedData as any).id; } let mutationInput; if (operation === TransformerMutationType.DELETE) { // For DELETE mutations, only the key(s) are included in the input - mutationInput = deleteInput; + mutationInput = deleteInput as ModelInstanceMetadata; } else { // Otherwise, we construct the mutation input with the following logic mutationInput = {}; @@ -598,7 +605,7 @@ class MutationProcessor { // scalar fields / non-model types if (operation === TransformerMutationType.UPDATE) { - if (!parsedData.hasOwnProperty(name)) { + if (!Object.prototype.hasOwnProperty.call(parsedData, name)) { // for update mutations - strip out a field if it's unchanged continue; } @@ -615,7 +622,7 @@ class MutationProcessor { _version, }; - const graphQLCondition = JSON.parse(condition); + const graphQLCondition = JSON.parse(condition) as GraphQLCondition; const variables = { input, @@ -628,6 +635,7 @@ class MutationProcessor { : null, }), }; + return [query, variables, graphQLCondition, opName, modelDefinition]; } diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index 6e90ed7de32..ac3760255d0 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -3,53 +3,49 @@ import { GraphQLResult } from '@aws-amplify/api'; import { InternalAPI } from '@aws-amplify/api/internals'; import { + ConsoleLogger, Hub, HubCapsule, fetchAuthSession, - ConsoleLogger, } from '@aws-amplify/core'; import { + BackgroundProcessManager, Category, CustomUserAgentDetails, DataStoreAction, - BackgroundProcessManager, GraphQLAuthMode, - AmplifyError, JwtPayload, } from '@aws-amplify/core/internals/utils'; - import { Observable, Observer, SubscriptionLike } from 'rxjs'; +import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/api-graphql'; + import { + AmplifyContext, + AuthModeStrategy, + ErrorHandler, InternalSchema, + ModelPredicate, PersistentModel, - SchemaModel, - SchemaNamespace, PredicatesGroup, - ModelPredicate, - AuthModeStrategy, - ErrorHandler, ProcessName, - AmplifyContext, + SchemaModel, + SchemaNamespace, } from '../../types'; import { + RTFError, + TransformerMutationType, buildSubscriptionGraphQLOperation, + generateRTFRemediation, getAuthorizationRules, getModelAuthModes, - getUserGroupsFromToken, - TransformerMutationType, getTokenForCustomAuth, + getUserGroupsFromToken, predicateToGraphQLFilter, - dynamicAuthFields, - filterFields, - repeatedFieldInGroup, - countFilterCombinations, - RTFError, - generateRTFRemediation, } from '../utils'; import { ModelPredicateCreator } from '../../predicates'; import { validatePredicate } from '../../util'; + import { getSubscriptionErrorType } from './errorMaps'; -import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/api-graphql'; const logger = new ConsoleLogger('DataStore'); @@ -63,20 +59,22 @@ export enum USER_CREDENTIALS { 'auth', } -type AuthorizationInfo = { +interface AuthorizationInfo { authMode: GraphQLAuthMode; isOwner: boolean; ownerField?: string; ownerValue?: string; -}; +} class SubscriptionProcessor { private readonly typeQuery = new WeakMap< SchemaModel, [TransformerMutationType, string, string][] >(); + private buffer: [TransformerMutationType, SchemaModel, PersistentModel][] = []; + private dataObserver!: Observer; private runningProcesses = new BackgroundProcessManager(); @@ -102,7 +100,7 @@ class SubscriptionProcessor { userCredentials: USER_CREDENTIALS, oidcTokenPayload: JwtPayload | undefined, authMode: GraphQLAuthMode, - filterArg: boolean = false, + filterArg = false, ): { opType: TransformerMutationType; opName: string; @@ -130,6 +128,7 @@ class SubscriptionProcessor { ownerField!, filterArg, ); + return { authMode, opType, opName, query, isOwner, ownerField, ownerValue }; } @@ -164,6 +163,7 @@ class SubscriptionProcessor { const validGroup = (authMode === 'oidc' || authMode === 'userPool') && + // eslint-disable-next-line array-callback-return groupAuthRules.find(groupAuthRule => { // validate token against groupClaim if (oidcTokenPayload) { @@ -233,7 +233,7 @@ class SubscriptionProcessor { } private hubQueryCompletionListener( - completed: Function, + completed: () => void, capsule: HubCapsule<'datastore', { event: string }>, ) { const { @@ -257,13 +257,14 @@ class SubscriptionProcessor { // Creating subs for each model/operation combo so they can be unsubscribed // independently, since the auth retry behavior is asynchronous. - let subscriptions: { - [modelName: string]: { + let subscriptions: Record< + string, + { [TransformerMutationType.CREATE]: SubscriptionLike[]; [TransformerMutationType.UPDATE]: SubscriptionLike[]; [TransformerMutationType.DELETE]: SubscriptionLike[]; - }; - } = {}; + } + > = {}; let oidcTokenPayload: JwtPayload | undefined; let userCredentials = USER_CREDENTIALS.none; this.runningProcesses.add(async () => { @@ -369,7 +370,7 @@ class SubscriptionProcessor { }; if (addFilter && predicatesGroup) { - variables['filter'] = + (variables as any).filter = predicateToGraphQLFilter(predicatesGroup); } @@ -378,6 +379,7 @@ class SubscriptionProcessor { observer.error( 'Owner field required, sign in is needed in order to perform this operation', ); + return; } @@ -390,18 +392,19 @@ class SubscriptionProcessor { }`, ); - const queryObservable = < - Observable>> - >(this.amplifyContext.InternalAPI.graphql( - { - query, - variables, - ...{ authMode }, - authToken, - }, - undefined, - customUserAgentDetails, - )); + const queryObservable = + this.amplifyContext.InternalAPI.graphql( + { + query, + variables, + ...{ authMode }, + authToken, + }, + undefined, + customUserAgentDetails, + ) as unknown as Observable< + GraphQLResult> + >; let subscriptionReadyCallback: (param?: unknown) => void; @@ -414,11 +417,11 @@ class SubscriptionProcessor { next: result => { const { data, errors } = result; if (Array.isArray(errors) && errors.length > 0) { - const messages = (< - { + const messages = ( + errors as { message: string; }[] - >errors).map(({ message }) => message); + ).map(({ message }) => message); logger.warn( `Skipping incoming subscription. Messages: ${messages.join( @@ -427,16 +430,16 @@ class SubscriptionProcessor { ); this.drainBuffer(); + return; } - const predicatesGroup = + const resolvedPredicatesGroup = ModelPredicateCreator.getPredicates( this.syncPredicates.get(modelDefinition)!, false, ); - // @ts-ignore const { [opName]: record } = data; // checking incoming subscription against syncPredicate. @@ -446,7 +449,7 @@ class SubscriptionProcessor { if ( this.passesPredicateValidation( record, - predicatesGroup!, + resolvedPredicatesGroup!, ) ) { this.pushToBuffer( @@ -461,6 +464,7 @@ class SubscriptionProcessor { const { errors: [{ message = '' } = {}], } = ({ + // eslint-disable-next-line no-empty-pattern errors: [], } = subscriptionError); @@ -488,6 +492,7 @@ class SubscriptionProcessor { // retry subscription connection without filter subscriptionRetry(operation, false); + return; } @@ -537,6 +542,7 @@ class SubscriptionProcessor { }`, ); subscriptionRetry(operation); + return; } } @@ -544,6 +550,7 @@ class SubscriptionProcessor { logger.warn('subscriptionError', message); try { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await 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', @@ -583,11 +590,11 @@ class SubscriptionProcessor { (async () => { let boundFunction: any; let removeBoundFunctionListener: () => void; - await new Promise(res => { - subscriptionReadyCallback = res; + await new Promise(resolve => { + subscriptionReadyCallback = resolve; boundFunction = this.hubQueryCompletionListener.bind( this, - res, + resolve, ); removeBoundFunctionListener = Hub.listen( 'api', @@ -615,13 +622,19 @@ class SubscriptionProcessor { return this.runningProcesses.addCleaner(async () => { Object.keys(subscriptions).forEach(modelName => { subscriptions[modelName][TransformerMutationType.CREATE].forEach( - subscription => subscription.unsubscribe(), + subscription => { + subscription.unsubscribe(); + }, ); subscriptions[modelName][TransformerMutationType.UPDATE].forEach( - subscription => subscription.unsubscribe(), + subscription => { + subscription.unsubscribe(); + }, ); subscriptions[modelName][TransformerMutationType.DELETE].forEach( - subscription => subscription.unsubscribe(), + subscription => { + subscription.unsubscribe(); + }, ); }); }); @@ -669,7 +682,9 @@ class SubscriptionProcessor { private drainBuffer() { if (this.dataObserver) { - this.buffer.forEach(data => this.dataObserver.next!(data)); + this.buffer.forEach(data => { + this.dataObserver.next!(data); + }); this.buffer = []; } } @@ -711,6 +726,7 @@ class SubscriptionProcessor { ); logger.warn(`${header}\n${message}\n${remediationMessage}`); + return true; } diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index d11ca8d4b82..319e153cb50 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -4,40 +4,40 @@ import { GraphQLResult } from '@aws-amplify/api'; import { InternalAPI } from '@aws-amplify/api/internals'; import { Observable } from 'rxjs'; import { + BackgroundProcessManager, + Category, + CustomUserAgentDetails, + DataStoreAction, + GraphQLAuthMode, + NonRetryableError, + jitteredExponentialRetry, +} from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger, Hub } from '@aws-amplify/core'; + +import { + AmplifyContext, + AuthModeStrategy, + ErrorHandler, + GraphQLFilter, InternalSchema, ModelInstanceMetadata, - SchemaModel, ModelPredicate, PredicatesGroup, - GraphQLFilter, - AuthModeStrategy, - ErrorHandler, ProcessName, - AmplifyContext, + SchemaModel, } from '../../types'; import { buildGraphQLOperation, - getModelAuthModes, getClientSideAuthError, getForbiddenError, - predicateToGraphQLFilter, + getModelAuthModes, getTokenForCustomAuth, + predicateToGraphQLFilter, } from '../utils'; -import { - jitteredExponentialRetry, - Category, - CustomUserAgentDetails, - DataStoreAction, - NonRetryableError, - BackgroundProcessManager, - GraphQLAuthMode, - AmplifyError, -} from '@aws-amplify/core/internals/utils'; - -import { Amplify, ConsoleLogger, Hub } from '@aws-amplify/core'; - import { ModelPredicateCreator } from '../../predicates'; + import { getSyncErrorType } from './errorMaps'; + const opResultDefaults = { items: [], nextToken: null, @@ -149,6 +149,7 @@ class SyncProcessor { logger.debug( `Sync successful with authMode: ${readAuthModes[authModeAttempts]}`, ); + return response; } catch (error) { authModeAttempts++; @@ -174,7 +175,8 @@ class SyncProcessor { readAuthModes[authModeAttempts - 1] }. Retrying with authMode: ${readAuthModes[authModeAttempts]}`, ); - return await authModeRetry(); + + return authModeRetry(); } }; @@ -206,16 +208,19 @@ class SyncProcessor { authMode: GraphQLAuthMode; onTerminate: Promise; }): Promise< - GraphQLResult<{ - [opName: string]: { - items: T[]; - nextToken: string; - startedAt: number; - }; - }> + GraphQLResult< + Record< + string, + { + items: T[]; + nextToken: string; + startedAt: number; + } + > + > > { - return await jitteredExponentialRetry( - async (query, variables) => { + return jitteredExponentialRetry( + async (retriedQuery, retriedVariables) => { try { const authToken = await getTokenForCustomAuth( authMode, @@ -229,8 +234,8 @@ class SyncProcessor { return await this.amplifyContext.InternalAPI.graphql( { - query, - variables, + query: retriedQuery, + variables: retriedVariables, authMode, authToken, }, @@ -275,6 +280,7 @@ class SyncProcessor { await Promise.all( otherErrors.map(async err => { try { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await 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', @@ -368,6 +374,7 @@ class SyncProcessor { const typeLastSync = typesLastSync.get(namespace.models[modelName]); map.set(namespace.models[modelName], typeLastSync!); } + return map; }, new Map(), @@ -394,7 +401,8 @@ class SyncProcessor { parentPromises.get(`${namespace}_${parent}`), ); - const promise = new Promise(async res => { + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async resolve => { await Promise.all(promises); do { @@ -407,7 +415,10 @@ class SyncProcessor { logger.debug( `Sync processor has been stopped, terminating sync for ${modelDefinition.name}`, ); - return res(); + + resolve(); + + return; } const limit = Math.min( @@ -431,6 +442,7 @@ class SyncProcessor { )); } catch (error) { try { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await 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', @@ -472,7 +484,7 @@ class SyncProcessor { }); } while (!done); - res(); + resolve(); }); parentPromises.set( @@ -500,13 +512,13 @@ class SyncProcessor { } } -export type SyncModelPage = { +export interface SyncModelPage { namespace: string; modelDefinition: SchemaModel; items: ModelInstanceMetadata[]; startedAt: number; done: boolean; isFullSync: boolean; -}; +} export { SyncProcessor }; diff --git a/packages/datastore/src/sync/utils.ts b/packages/datastore/src/sync/utils.ts index ec6c4adf751..2e538ab0efa 100644 --- a/packages/datastore/src/sync/utils.ts +++ b/packages/datastore/src/sync/utils.ts @@ -3,53 +3,55 @@ import { GraphQLAuthError } from '@aws-amplify/api'; import type { GraphQLError } from 'graphql'; import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; + import { ModelInstanceCreator } from '../datastore/datastore'; import { + AuthModeStrategy, AuthorizationRule, GraphQLCondition, - GraphQLFilter, GraphQLField, - isEnumFieldType, - isGraphQLScalarType, - isPredicateObj, - isSchemaModel, - isSchemaModelWithAttributes, - isTargetNameAssociation, - isNonModelFieldType, + GraphQLFilter, + InternalSchema, + ModelAttributes, ModelFields, ModelInstanceMetadata, + ModelOperation, OpType, PersistentModel, PersistentModelConstructor, - PredicatesGroup, PredicateObject, + PredicatesGroup, RelationshipType, SchemaModel, SchemaNamespace, SchemaNonModel, - ModelOperation, - InternalSchema, - AuthModeStrategy, - ModelAttributes, + isEnumFieldType, + isGraphQLScalarType, + isNonModelFieldType, isPredicateGroup, + isPredicateObj, + isSchemaModel, + isSchemaModelWithAttributes, + isTargetNameAssociation, } from '../types'; import { - extractPrimaryKeyFieldNames, - establishRelationAndKeys, IDENTIFIER_KEY_SEPARATOR, + establishRelationAndKeys, + extractPrimaryKeyFieldNames, } from '../util'; + import { MutationEvent } from './'; -import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('DataStore'); -enum GraphQLOperationType { - LIST = 'query', - CREATE = 'mutation', - UPDATE = 'mutation', - DELETE = 'mutation', - GET = 'query', -} +const GraphQLOperationType = { + LIST: 'query', + CREATE: 'mutation', + UPDATE: 'mutation', + DELETE: 'mutation', + GET: 'query', +}; export enum TransformerMutationType { CREATE = 'Create', @@ -64,10 +66,10 @@ const dummyMetadata: ModelInstanceMetadata = { _deleted: undefined!, }; -const metadataFields = <(keyof ModelInstanceMetadata)[]>( - Object.keys(dummyMetadata) -); -export function getMetadataFields(): ReadonlyArray { +const metadataFields = Object.keys( + dummyMetadata, +) as (keyof ModelInstanceMetadata)[]; +export function getMetadataFields(): readonly string[] { return metadataFields; } @@ -107,6 +109,7 @@ function getImplicitOwnerField( if (!scalarFields.owner && ownerFields.includes('owner')) { return ['owner']; } + return []; } @@ -117,13 +120,16 @@ function getOwnerFields( if (isSchemaModelWithAttributes(modelDefinition)) { modelDefinition.attributes!.forEach(attr => { if (attr.properties && attr.properties.rules) { - const rule = attr.properties.rules.find(rule => rule.allow === 'owner'); + const rule = attr.properties.rules.find( + currentRule => currentRule.allow === 'owner', + ); if (rule && rule.ownerField) { ownerFields.push(rule.ownerField); } } }); } + return ownerFields; } @@ -173,11 +179,12 @@ function getConnectionFields( // Need to retrieve relations in order to get connected model keys const [relations] = establishRelationAndKeys(namespace); - const connectedModelName = - modelDefinition.fields[name].type['model']; + const connectedModelName = ( + modelDefinition.fields[name].type as any + ).model; const byPkIndex = relations[connectedModelName].indexes.find( - ([name]) => name === 'byPk', + ([currentName]) => currentName === 'byPk', ); const keyFields = byPkIndex && byPkIndex[1]; const keyFieldSelectionSet = keyFields?.join(' '); @@ -208,17 +215,18 @@ function getNonModelFields( if (isNonModelFieldType(type)) { const typeDefinition = namespace.nonModels![type.nonModel]; const scalarFields = Object.values(getScalarFields(typeDefinition)).map( - ({ name }) => name, + ({ name: currentName }) => currentName, ); const nested: string[] = []; Object.values(typeDefinition.fields).forEach(field => { - const { type, name } = field; + const { type: fieldType, name: fieldName } = field; - if (isNonModelFieldType(type)) { - const typeDefinition = namespace.nonModels![type.nonModel]; + if (isNonModelFieldType(fieldType)) { + const nonModelTypeDefinition = + namespace.nonModels![fieldType.nonModel]; nested.push( - `${name} { ${generateSelectionSet(namespace, typeDefinition)} }`, + `${fieldName} { ${generateSelectionSet(namespace, nonModelTypeDefinition)} }`, ); } }); @@ -293,6 +301,7 @@ export function getAuthorizationRules( if (isOwnerAuth) { // owner rules has least priority resultRules.push(authRule); + return; } @@ -308,7 +317,7 @@ export function buildSubscriptionGraphQLOperation( transformerMutationType: TransformerMutationType, isOwnerAuthorization: boolean, ownerField: string, - filterArg: boolean = false, + filterArg = false, ): [TransformerMutationType, string, string] { const selectionSet = generateSelectionSet(namespace, modelDefinition); @@ -453,6 +462,7 @@ export function createMutationInstanceFromModelOperation< if (isAWSJSON) { return JSON.stringify(v); } + return v; }; @@ -491,12 +501,13 @@ export function predicateToGraphQLCondition( // key fields from the predicate/condition when ALL of the keyFields are present and using `eq` operators const keyFields = extractPrimaryKeyFieldNames(modelDefinition); + return predicateToGraphQLFilter(predicate, keyFields) as GraphQLCondition; } /** * @param predicatesGroup - Predicate Group @returns GQL Filter Expression from Predicate Group - + @remarks Flattens redundant list predicates @example @@ -537,6 +548,7 @@ export function predicateToGraphQLFilter( }; children.push(gqlField); + return; } @@ -557,6 +569,7 @@ export function predicateToGraphQLFilter( ) { delete result[type]; Object.assign(result, child); + return result; } } @@ -693,6 +706,7 @@ export function repeatedFieldInGroup( } seen[fieldName] = true; } + return null; }; @@ -779,6 +793,7 @@ export function generateRTFRemediation( `Dynamic auth modes, such as owner auth and dynamic group auth factor in to the number of combinations you're using.\n` + `You currently have ${dynamicAuthModeFields.size} dynamic auth mode(s) configured on this model: ${dynamicAuthFieldsStr}.`; } + return message; } @@ -796,7 +811,7 @@ export function generateRTFRemediation( } export function getUserGroupsFromToken( - token: { [field: string]: any }, + token: Record, rule: AuthorizationRule, ): string[] { // validate token against groupClaim @@ -861,6 +876,7 @@ export async function getModelAuthModes({ } catch (error) { logger.debug(`Error getting auth modes for model: ${modelName}`, error); } + return modelAuthModes; } @@ -883,12 +899,13 @@ export function getForbiddenError(error) { )}` ); } + return null; } export function resolveServiceErrorStatusCode(error: unknown): number | null { - if (error?.['$metadata']?.['httpStatusCode']) { - return Number(error?.['$metadata']?.['httpStatusCode']); + if ((error as any)?.$metadata?.httpStatusCode) { + return Number((error as any)?.$metadata?.httpStatusCode); } else if ((error as GraphQLError)?.originalError) { return resolveServiceErrorStatusCode( (error as GraphQLError)?.originalError, @@ -906,6 +923,7 @@ export function getClientSideAuthError(error) { clientSideAuthErrors.find(clientError => error.message.includes(clientError), ); + return clientSideError || null; } @@ -920,6 +938,7 @@ export async function getTokenForCustomAuth( if (functionAuthProvider && typeof functionAuthProvider === 'function') { try { const { token } = await functionAuthProvider(); + return token; } catch (error) { throw new Error( diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index e443726cdd7..75e0b9ff27e 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -1,27 +1,28 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { InternalAPI } from '@aws-amplify/api/internals'; +import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; + import { ModelInstanceCreator } from './datastore/datastore'; import { + NAMESPACES, + extractPrimaryKeyFieldNames, isAWSDate, - isAWSTime, isAWSDateTime, - isAWSTimestamp, isAWSEmail, + isAWSIPAddress, isAWSJSON, - isAWSURL, isAWSPhone, - isAWSIPAddress, - NAMESPACES, - extractPrimaryKeyFieldNames, + isAWSTime, + isAWSTimestamp, + isAWSURL, } from './util'; import { PredicateAll } from './predicates'; -import { InternalAPI } from '@aws-amplify/api/internals'; import { Adapter } from './storage/adapter'; -import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; -export type Scalar = T extends Array ? InnerType : T; +export type Scalar = T extends (infer InnerType)[] ? InnerType : T; -//#region Schema types +// #region Schema types /** * @deprecated If you intended to use the Schema for `generateClient`, then you've imported the wrong Schema type. * Use `import { type Schema } from '../amplify/data/resource' instead. If you intended to import the type for DataStore @@ -34,25 +35,25 @@ export type DataStoreSchema = UserSchema & { codegenVersion: string; }; -export type UserSchema = { +export interface UserSchema { models: SchemaModels; nonModels?: SchemaNonModels; relationships?: RelationshipType; keys?: ModelKeys; enums: SchemaEnums; modelTopologicalOrdering?: Map; -}; -export type InternalSchema = { +} +export interface InternalSchema { namespaces: SchemaNamespaces; version: string; codegenVersion: string; -}; +} export type SchemaNamespaces = Record; export type SchemaNamespace = UserSchema & { name: string; }; export type SchemaModels = Record; -export type SchemaModel = { +export interface SchemaModel { name: string; pluralName: string; attributes?: ModelAttributes; @@ -68,10 +69,10 @@ export type SchemaModel = { allFields?: ModelFields; syncable?: boolean; -}; +} export function isSchemaModel(obj: any): obj is SchemaModel { - return obj && (obj).pluralName !== undefined; + return obj && (obj as SchemaModel).pluralName !== undefined; } export function isSchemaModelWithAttributes( @@ -81,37 +82,37 @@ export function isSchemaModelWithAttributes( } export type SchemaNonModels = Record; -export type SchemaNonModel = { +export interface SchemaNonModel { name: string; fields: ModelFields; -}; +} type SchemaEnums = Record; -type SchemaEnum = { +interface SchemaEnum { name: string; values: string[]; -}; -export type ModelMeta = { +} +export interface ModelMeta { builder: PersistentModelConstructor; schema: SchemaModel; pkField: string[]; -}; +} export type ModelAssociation = AssociatedWith | TargetNameAssociation; -type AssociatedWith = { +interface AssociatedWith { connectionType: 'HAS_MANY' | 'HAS_ONE'; associatedWith: string | string[]; targetName?: string; targetNames?: string[]; -}; +} export function isAssociatedWith(obj: any): obj is AssociatedWith { return obj && obj.associatedWith; } -type TargetNameAssociation = { +interface TargetNameAssociation { connectionType: 'BELONGS_TO'; targetName?: string; targetNames?: string[]; -}; +} export function isTargetNameAssociation( obj: any, @@ -119,9 +120,9 @@ export function isTargetNameAssociation( return obj?.targetName || obj?.targetNames; } -type FieldAssociation = { +interface FieldAssociation { connectionType: 'HAS_ONE' | 'BELONGS_TO' | 'HAS_MANY'; -}; +} export function isFieldAssociation( obj: any, fieldName: string, @@ -130,9 +131,12 @@ export function isFieldAssociation( } export type ModelAttributes = ModelAttribute[]; -export type ModelAttribute = { type: string; properties?: Record }; +export interface ModelAttribute { + type: string; + properties?: Record; +} -export type ModelAuthRule = { +export interface ModelAuthRule { allow: string; provider?: string; operations?: string[]; @@ -141,14 +145,14 @@ export type ModelAuthRule = { groups?: string[]; groupClaim?: string; groupsField?: string; -}; +} -export type ModelAttributeAuth = { +export interface ModelAttributeAuth { type: 'auth'; properties: { rules: ModelAuthRule[]; }; -}; +} export function isModelAttributeAuth( attr: ModelAttribute, @@ -161,29 +165,29 @@ export function isModelAttributeAuth( ); } -type ModelAttributeKey = { +interface ModelAttributeKey { type: 'key'; properties: { name?: string; fields: string[]; }; -}; +} -type ModelAttributePrimaryKey = { +interface ModelAttributePrimaryKey { type: 'key'; properties: { name: never; fields: string[]; }; -}; +} -type ModelAttributeCompositeKey = { +interface ModelAttributeCompositeKey { type: 'key'; properties: { name: string; fields: [string, string, string, string?, string?]; }; -}; +} export function isModelAttributeKey( attr: ModelAttribute, @@ -212,7 +216,7 @@ export function isModelAttributeCompositeKey( ); } -export type ModelAttributeAuthProperty = { +export interface ModelAttributeAuthProperty { allow: ModelAttributeAuthAllow; identityClaim?: string; groupClaim?: string; @@ -220,7 +224,7 @@ export type ModelAttributeAuthProperty = { operations?: string[]; ownerField?: string; provider?: ModelAttributeAuthProvider; -}; +} export enum ModelAttributeAuthAllow { CUSTOM = 'custom', @@ -256,6 +260,7 @@ export enum GraphQLScalarType { AWSIPAddress, } +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace GraphQLScalarType { export function getJSType( scalar: keyof Omit< @@ -318,7 +323,7 @@ export namespace GraphQLScalarType { } } -export type AuthorizationRule = { +export interface AuthorizationRule { identityClaim: string; ownerField: string; provider: 'userPools' | 'oidc' | 'iam' | 'apiKey'; @@ -327,7 +332,7 @@ export type AuthorizationRule = { groupsField: string; authStrategy: 'owner' | 'groups' | 'private' | 'public'; areSubscriptionsPublic: boolean; -}; +} export function isGraphQLScalarType( obj: any, @@ -338,11 +343,11 @@ export function isGraphQLScalarType( return obj && GraphQLScalarType[obj] !== undefined; } -export type ModelFieldType = { +export interface ModelFieldType { model: string; modelConstructor?: ModelMeta; -}; -export function isModelFieldType( +} +export function isModelFieldType<_ extends PersistentModel>( obj: any, ): obj is ModelFieldType { const modelField: keyof ModelFieldType = 'model'; @@ -351,7 +356,9 @@ export function isModelFieldType( return false; } -export type NonModelFieldType = { nonModel: string }; +export interface NonModelFieldType { + nonModel: string; +} export function isNonModelFieldType(obj: any): obj is NonModelFieldType { const typeField: keyof NonModelFieldType = 'nonModel'; if (obj && obj[typeField]) return true; @@ -359,7 +366,9 @@ export function isNonModelFieldType(obj: any): obj is NonModelFieldType { return false; } -type EnumFieldType = { enum: string }; +interface EnumFieldType { + enum: string; +} export function isEnumFieldType(obj: any): obj is EnumFieldType { const modelField: keyof EnumFieldType = 'enum'; if (obj && obj[modelField]) return true; @@ -367,7 +376,7 @@ export function isEnumFieldType(obj: any): obj is EnumFieldType { return false; } -export type ModelField = { +export interface ModelField { name: string; type: | keyof Omit< @@ -383,22 +392,20 @@ export type ModelField = { isArrayNullable?: boolean; association?: ModelAssociation; attributes?: ModelAttributes[]; -}; -//#endregion +} +// #endregion -//#region Model definition -export type NonModelTypeConstructor = { - new (init: T): T; -}; +// #region Model definition +export type NonModelTypeConstructor = new (init: T) => T; // Class for model -export type PersistentModelConstructor = { +export interface PersistentModelConstructor { new (init: ModelInit>): T; copyOf( src: T, mutator: (draft: MutableModel>) => void, ): T; -}; +} /** * @private @@ -443,7 +450,7 @@ export type OptionallyManagedIdentifier = IdentifierBrand< >; // You provide the values -export type CompositeIdentifier> = IdentifierBrand< +export type CompositeIdentifier = IdentifierBrand< { fields: K; type: T }, 'CompositeIdentifier' >; @@ -494,10 +501,10 @@ export type IdentifierFieldsForInit< // Instance of model export declare const __modelMeta__: unique symbol; -export type PersistentModelMetaData = { +export interface PersistentModelMetaData { identifier?: Identifier; readOnlyFields?: string; -}; +} export interface AsyncCollection extends AsyncIterable { toArray(options?: { max?: number }): Promise; @@ -538,19 +545,11 @@ type OptionalRelativesOf = type OmitOptionalRelatives = Omit>; type PickOptionalRelatives = Pick>; -type OmitOptionalFields = Omit< - T, - KeysOfSuperType | OptionalRelativesOf ->; -type PickOptionalFields = Pick< - T, - KeysOfSuperType | OptionalRelativesOf ->; -export type DefaultPersistentModelMetaData = { +export interface DefaultPersistentModelMetaData { identifier: ManagedIdentifier<{ id: string }, 'id'>; readOnlyFields: never; -}; +} export type MetadataOrDefault< T extends PersistentModel, @@ -578,6 +577,7 @@ export type MetadataReadOnlyFields< // This type makes optional some identifiers in the constructor init object (e.g. OptionallyManagedIdentifier) export type ModelInitBase< T extends PersistentModel, + // eslint-disable-next-line @typescript-eslint/ban-types M extends PersistentModelMetaData = {}, > = Omit< T, @@ -592,6 +592,7 @@ export type ModelInitBase< export type ModelInit< T extends PersistentModel, + // eslint-disable-next-line @typescript-eslint/ban-types M extends PersistentModelMetaData = {}, > = { [P in keyof OmitOptionalRelatives>]: SettableFieldType< @@ -617,6 +618,7 @@ type DeepWritable = { export type MutableModel< T extends PersistentModel, + // eslint-disable-next-line @typescript-eslint/ban-types M extends PersistentModelMetaData = {}, // This provides Intellisense with ALL of the properties, regardless of read-only // but will throw a linting error if trying to overwrite a read-only property @@ -625,11 +627,11 @@ export type MutableModel< > & Readonly | MetadataReadOnlyFields>>; -export type ModelInstanceMetadata = { +export interface ModelInstanceMetadata { _version: number; _lastChangedAt: number; _deleted: boolean; -}; +} export type IdentifierFieldValue< T extends PersistentModel, @@ -656,9 +658,9 @@ export function isIdentifierObject( typeof obj === 'object' && obj && keys.every(k => obj[k] !== undefined) ); } -//#endregion +// #endregion -//#region Subscription messages +// #region Subscription messages export enum OpType { INSERT = 'INSERT', UPDATE = 'UPDATE', @@ -670,21 +672,21 @@ export type SubscriptionMessage = Pick< 'opType' | 'element' | 'model' | 'condition' >; -export type InternalSubscriptionMessage = { +export interface InternalSubscriptionMessage { opType: OpType; element: T; model: PersistentModelConstructor; condition: PredicatesGroup | null; savedElement?: T; -}; +} -export type DataStoreSnapshot = { +export interface DataStoreSnapshot { items: T[]; isSynced: boolean; -}; -//#endregion +} +// #endregion -//#region Predicates +// #region Predicates export type PredicateExpression = TypeName extends keyof MapTypeToOperands @@ -695,10 +697,10 @@ export type PredicateExpression = ) => ModelPredicate : never; -type EqualityOperators = { +interface EqualityOperators { ne: T; eq: T; -}; +} type ScalarNumberOperators = EqualityOperators & { le: T; lt: T; @@ -714,22 +716,22 @@ type StringOperators = ScalarNumberOperators & { notContains: T; }; type BooleanOperators = EqualityOperators; -type ArrayOperators = { +interface ArrayOperators { contains: T; notContains: T; -}; +} export type AllOperators = NumberOperators & StringOperators & ArrayOperators; -type MapTypeToOperands = { +interface MapTypeToOperands { number: NumberOperators>; string: StringOperators>; boolean: BooleanOperators>; 'number[]': ArrayOperators; 'string[]': ArrayOperators; 'boolean[]': ArrayOperators; -}; +} type TypeName = T extends string ? 'string' @@ -745,17 +747,17 @@ type TypeName = T extends string ? 'boolean[]' : never; -export type PredicateGroups = { - and: ( +export interface PredicateGroups { + and( predicate: (predicate: ModelPredicate) => ModelPredicate, - ) => ModelPredicate; - or: ( + ): ModelPredicate; + or( predicate: (predicate: ModelPredicate) => ModelPredicate, - ) => ModelPredicate; - not: ( + ): ModelPredicate; + not( predicate: (predicate: ModelPredicate) => ModelPredicate, - ) => ModelPredicate; -}; + ): ModelPredicate; +} export type ModelPredicate = { [K in keyof M]-?: PredicateExpression>; @@ -765,38 +767,37 @@ export type ProducerModelPredicate = ( condition: ModelPredicate, ) => ModelPredicate; -export type PredicatesGroup = { +export interface PredicatesGroup { type: keyof PredicateGroups; predicates: (PredicateObject | PredicatesGroup)[]; -}; +} export function isPredicateObj( obj: any, ): obj is PredicateObject { - return obj && (>obj).field !== undefined; + return obj && (obj as PredicateObject).field !== undefined; } export function isPredicateGroup( obj: any, ): obj is PredicatesGroup { - return obj && (>obj).type !== undefined; + return obj && (obj as PredicatesGroup).type !== undefined; } -export type PredicateObject = { +export interface PredicateObject { field: keyof T; operator: keyof AllOperators; operand: any; -}; +} export enum QueryOne { FIRST, LAST, } -export type GraphQLField = { - [field: string]: { - [operator: string]: string | number | [number, number]; - }; -}; +export type GraphQLField = Record< + string, + Record +>; export type GraphQLCondition = Partial< | GraphQLField @@ -820,26 +821,26 @@ export type GraphQLFilter = Partial< } >; -//#endregion +// #endregion -//#region Pagination +// #region Pagination -export type ProducerPaginationInput = { +export interface ProducerPaginationInput { sort?: ProducerSortPredicate; limit?: number; page?: number; -}; +} export type ObserveQueryOptions = Pick< ProducerPaginationInput, 'sort' >; -export type PaginationInput = { +export interface PaginationInput { sort?: SortPredicate; limit?: number; page?: number; -}; +} export type ProducerSortPredicate = ( condition: SortPredicate, @@ -862,16 +863,16 @@ export enum SortDirection { export type SortPredicatesGroup = SortPredicateObject[]; -export type SortPredicateObject = { +export interface SortPredicateObject { field: keyof T; sortDirection: keyof typeof SortDirection; -}; +} -//#endregion +// #endregion -//#region System Components +// #region System Components -export type SystemComponent = { +export interface SystemComponent { setUp( schema: InternalSchema, namespaceResolver: NamespaceResolver, @@ -882,62 +883,61 @@ export type SystemComponent = { ) => PersistentModelConstructor, appId?: string, ): Promise; -}; +} export type NamespaceResolver = ( modelConstructor: PersistentModelConstructor, ) => string; -export type ControlMessageType = { +export interface ControlMessageType { type: T; data?: any; -}; +} -//#endregion +// #endregion -//#region Relationship types -export type RelationType = { +// #region Relationship types +export interface RelationType { fieldName: string; modelName: string; relationType: 'HAS_ONE' | 'HAS_MANY' | 'BELONGS_TO'; targetName?: string; targetNames?: string[]; associatedWith?: string | string[]; -}; +} -type IndexOptions = { +interface IndexOptions { unique?: boolean; -}; +} -export type IndexesType = Array<[string, string[], IndexOptions?]>; +export type IndexesType = [string, string[], IndexOptions?][]; -export type RelationshipType = { - [modelName: string]: { +export type RelationshipType = Record< + string, + { indexes: IndexesType; relationTypes: RelationType[]; - }; -}; + } +>; -//#endregion +// #endregion -//#region Key type -export type KeyType = { +// #region Key type +export interface KeyType { primaryKey?: string[]; compositeKeys?: Set[]; -}; +} -export type ModelKeys = { - [modelName: string]: KeyType; -}; +export type ModelKeys = Record; -//#endregion +// #endregion -//#region DataStore config types -export type DataStoreConfig = { +// #region DataStore config types +export interface DataStoreConfig { DataStore?: { authModeStrategyType?: AuthModeStrategyType; conflictHandler?: ConflictHandler; // default : retry until client wins up to x times - errorHandler?: (error: SyncError) => void; // default : logger.warn + errorHandler?(error: SyncError): void; // default : logger.warn maxRecordsToSync?: number; // merge syncPageSize?: number; fullSyncInterval?: number; @@ -947,18 +947,18 @@ export type DataStoreConfig = { }; authModeStrategyType?: AuthModeStrategyType; conflictHandler?: ConflictHandler; // default : retry until client wins up to x times - errorHandler?: (error: SyncError) => void; // default : logger.warn + errorHandler?(error: SyncError): void; // default : logger.warn maxRecordsToSync?: number; // merge syncPageSize?: number; fullSyncInterval?: number; syncExpressions?: SyncExpression[]; authProviders?: AuthProviders; storageAdapter?: Adapter; -}; +} -export type AuthProviders = { - functionAuthProvider: () => { token: string } | Promise<{ token: string }>; -}; +export interface AuthProviders { + functionAuthProvider(): { token: string } | Promise<{ token: string }>; +} export enum AuthModeStrategyType { DEFAULT = 'DEFAULT', @@ -971,11 +971,11 @@ export type AuthModeStrategyReturn = | undefined | null; -export type AuthModeStrategyParams = { +export interface AuthModeStrategyParams { schema: InternalSchema; modelName: string; operation: ModelOperation; -}; +} export type AuthModeStrategy = ( authModeStrategyParams: AuthModeStrategyParams, @@ -997,7 +997,7 @@ export type ModelAuthModes = Record< export type SyncExpression = Promise<{ modelConstructor: any; - conditionProducer: (c?: any) => any; + conditionProducer(c?: any): any; }>; /* @@ -1019,14 +1019,14 @@ type Option0 = []; type Option1 = [V5ModelPredicate | undefined]; type Option = Option0 | Option1; -type Lookup = { +interface Lookup { 0: | ModelPredicateExtender | Promise> | typeof PredicateAll | Promise; 1: PredicateInternalsKey | undefined; -}; +} type ConditionProducer> = ( ...args: A @@ -1048,15 +1048,15 @@ export async function syncExpression< }; } -export type SyncConflict = { +export interface SyncConflict { modelConstructor: PersistentModelConstructor; localModel: PersistentModel; remoteModel: PersistentModel; operation: OpType; attempts: number; -}; +} -export type SyncError = { +export interface SyncError { message: string; errorType: ErrorType; errorInfo?: string; @@ -1067,7 +1067,7 @@ export type SyncError = { process: ProcessName; operation: string; cause?: Error; -}; +} export type ErrorType = | 'ConfigError' @@ -1093,21 +1093,21 @@ export type ConflictHandler = ( | typeof DISCARD; export type ErrorHandler = (error: SyncError) => void; -export type DeferredCallbackResolverOptions = { - callback: () => void; +export interface DeferredCallbackResolverOptions { + callback(): void; maxInterval?: number; - errorHandler?: (error: string) => void; -}; + errorHandler?(error: string): void; +} export enum LimitTimerRaceResolvedValues { LIMIT = 'LIMIT', TIMER = 'TIMER', } -//#endregion +// #endregion -export type AmplifyContext = { +export interface AmplifyContext { InternalAPI: typeof InternalAPI; -}; +} // #region V5 predicate types @@ -1206,7 +1206,7 @@ export type ModelPredicateAggregateExtender = ( ) => PredicateInternalsKey[]; export type ValuePredicate< - RT extends PersistentModel, + _RT extends PersistentModel, MT extends MatchableTypes, > = { [K in AllFieldOperators]: K extends 'between' @@ -1240,7 +1240,7 @@ export type ModelPredicateNegation = ( * that should not be exposed on public customer interfaces. */ export class PredicateInternalsKey { - private __isPredicateInternalsKeySentinel: boolean = true; + private __isPredicateInternalsKeySentinel = true; } // #endregion diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index 684bda822c1..cd33533ebef 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -1,40 +1,41 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { monotonicFactory, ULID } from 'ulid'; +import { ULID, monotonicFactory } from 'ulid'; import { - amplifyUuid, AmplifyUrl, WordArray, + amplifyUuid, } from '@aws-amplify/core/internals/utils'; -import { produce, applyPatches, Patch } from 'immer'; +import { Patch, applyPatches, produce } from 'immer'; + import { ModelInstanceCreator } from './datastore/datastore'; import { AllOperators, - isPredicateGroup, - isPredicateObj, + DeferredCallbackResolverOptions, + IndexesType, + LimitTimerRaceResolvedValues, + ModelAssociation, + ModelAttribute, + ModelAttributes, + ModelKeys, + NonModelTypeConstructor, + PaginationInput, PersistentModel, PersistentModelConstructor, PredicateGroups, PredicateObject, PredicatesGroup, - RelationshipType, RelationType, - ModelKeys, - ModelAttributes, + RelationshipType, + SchemaModel, SchemaNamespace, - SortPredicatesGroup, SortDirection, + SortPredicatesGroup, + isModelAttributeCompositeKey, isModelAttributeKey, isModelAttributePrimaryKey, - isModelAttributeCompositeKey, - NonModelTypeConstructor, - PaginationInput, - DeferredCallbackResolverOptions, - LimitTimerRaceResolvedValues, - SchemaModel, - ModelAttribute, - IndexesType, - ModelAssociation, + isPredicateGroup, + isPredicateObj, } from './types'; import { ModelSortPredicateCreator } from './predicates'; @@ -73,14 +74,14 @@ export enum NAMESPACES { STORAGE = 'storage', } -const DATASTORE = NAMESPACES.DATASTORE; -const USER = NAMESPACES.USER; -const SYNC = NAMESPACES.SYNC; -const STORAGE = NAMESPACES.STORAGE; +const { DATASTORE } = NAMESPACES; +const { USER } = NAMESPACES; +const { SYNC } = NAMESPACES; +const { STORAGE } = NAMESPACES; export { USER, SYNC, STORAGE, DATASTORE }; -export const exhaustiveCheck = (obj: never, throwOnError: boolean = true) => { +export const exhaustiveCheck = (obj: never, throwOnError = true) => { if (throwOnError) { throw new Error(`Invalid ${obj}`); } @@ -127,6 +128,7 @@ export const validatePredicate = ( if (isPredicateGroup(predicateOrGroup)) { const { type, predicates } = predicateOrGroup; + return validatePredicate(model, type, predicates); } @@ -154,23 +156,26 @@ export const validatePredicateField = ( return value >= operand; case 'gt': return value > operand; - case 'between': - const [min, max] = <[T, T]>operand; + case 'between': { + const [min, max] = operand as [T, T]; + return value >= min && value <= max; + } case 'beginsWith': return ( !isNullOrUndefined(value) && - ((value)).startsWith((operand)) + (value as unknown as string).startsWith(operand as unknown as string) ); case 'contains': return ( !isNullOrUndefined(value) && - ((value)).indexOf((operand)) > -1 + (value as unknown as string).indexOf(operand as unknown as string) > -1 ); case 'notContains': return ( isNullOrUndefined(value) || - ((value)).indexOf((operand)) === -1 + (value as unknown as string).indexOf(operand as unknown as string) === + -1 ); default: return false; @@ -181,7 +186,7 @@ export const isModelConstructor = ( obj: any, ): obj is PersistentModelConstructor => { return ( - obj && typeof (>obj).copyOf === 'function' + obj && typeof (obj as PersistentModelConstructor).copyOf === 'function' ); }; @@ -220,7 +225,9 @@ export const traverseModel = ( instance: T; }[] = []; - const newInstance = modelConstructor.copyOf(instance, () => {}); + const newInstance = modelConstructor.copyOf(instance, () => { + // no-op + }); result.unshift({ modelName: srcModelName, @@ -251,6 +258,7 @@ let privateModeCheckResult; export const isPrivateMode = () => { return new Promise(resolve => { const dbname = amplifyUuid(); + // eslint-disable-next-line prefer-const let db; const isPrivate = () => { @@ -268,7 +276,7 @@ export const isPrivateMode = () => { privateModeCheckResult = true; - return resolve(false); + resolve(false); }; if (privateModeCheckResult === true) { @@ -276,10 +284,16 @@ export const isPrivateMode = () => { } if (privateModeCheckResult === false) { - return isPrivate(); + isPrivate(); + + return; } - if (indexedDB === null) return isPrivate(); + if (indexedDB === null) { + isPrivate(); + + return; + } db = indexedDB.open(dbname); db.onerror = isPrivate; @@ -313,19 +327,23 @@ export const isSafariCompatabilityMode: () => Promise = async () => { const db: IDBDatabase | false = await new Promise(resolve => { const dbOpenRequest = indexedDB.open(dbName); - dbOpenRequest.onerror = () => resolve(false); + dbOpenRequest.onerror = () => { + resolve(false); + }; dbOpenRequest.onsuccess = () => { - const db = dbOpenRequest.result; - resolve(db); + const openedDb = dbOpenRequest.result; + resolve(openedDb); }; dbOpenRequest.onupgradeneeded = (event: any) => { - const db = event?.target?.result; + const upgradedDb = event?.target?.result; - db.onerror = () => resolve(false); + upgradedDb.onerror = () => { + resolve(false); + }; - const store = db.createObjectStore(storeName, { + const store = upgradedDb.createObjectStore(storeName, { autoIncrement: true, }); @@ -352,7 +370,9 @@ export const isSafariCompatabilityMode: () => Promise = async () => { const getRequest = index.get([1]); - getRequest.onerror = () => resolve(false); + getRequest.onerror = () => { + resolve(false); + }; getRequest.onsuccess = (event: any) => { resolve(event?.target?.result); @@ -360,6 +380,7 @@ export const isSafariCompatabilityMode: () => Promise = async () => { }); if (db && typeof db.close === 'function') { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await db.close(); } @@ -486,7 +507,7 @@ export function sortCompareFunction( export function directedValueEquality( fromObject: object, againstObject: object, - nullish: boolean = false, + nullish = false, ) { const aKeys = Object.keys(fromObject); @@ -507,11 +528,7 @@ export function directedValueEquality( // returns true if equal by value // if nullish is true, treat undefined and null values as equal // to normalize for GQL response values for undefined fields -export function valuesEqual( - valA: any, - valB: any, - nullish: boolean = false, -): boolean { +export function valuesEqual(valA: any, valB: any, nullish = false): boolean { let a = valA; let b = valB; @@ -610,6 +627,7 @@ export function inMemoryPagination( return records.slice(start, end); } + return records; } @@ -629,6 +647,7 @@ export async function asyncSome( return true; } } + return false; } @@ -648,6 +667,7 @@ export async function asyncEvery( return false; } } + return true; } @@ -669,6 +689,7 @@ export async function asyncFilter( results.push(item); } } + return results; } @@ -701,6 +722,7 @@ export const isAWSEmail = (val: string): boolean => { export const isAWSJSON = (val: string): boolean => { try { JSON.parse(val); + return true; } catch { return false; @@ -730,6 +752,7 @@ export class DeferredPromise { public resolve: (value: string | PromiseLike) => void; public reject: () => void; constructor() { + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; this.promise = new Promise( (resolve: (value: string | PromiseLike) => void, reject) => { @@ -746,7 +769,10 @@ export class DeferredCallbackResolver { private maxInterval: number; private timer: ReturnType; private raceInFlight = false; - private callback = () => {}; + private callback = () => { + // no-op + }; + private errorHandler: (error: string) => void; private defaultErrorHandler = ( msg = 'DeferredCallbackResolver error', @@ -761,7 +787,7 @@ export class DeferredCallbackResolver { } private startTimer(): void { - this.timerPromise = new Promise((resolve, reject) => { + this.timerPromise = new Promise((resolve, _reject) => { this.timer = setTimeout(() => { resolve(LimitTimerRaceResolvedValues.TIMER); }, this.maxInterval); @@ -786,6 +812,7 @@ export class DeferredCallbackResolver { this.raceInFlight = false; this.limitPromise = new DeferredPromise(); + // eslint-disable-next-line no-unsafe-finally return winner!; } } @@ -836,6 +863,7 @@ export function mergePatches( patches = p; }, ); + return patches!; } @@ -845,7 +873,7 @@ export const getStorename = (namespace: string, modelName: string) => { return storeName; }; -//#region Key Utils +// #region Key Utils /* When we have GSI(s) with composite sort keys defined on a model @@ -903,6 +931,7 @@ export const processCompositeKeys = ( if (combined.length === 0) { combined.push(sortKeyFieldsSet); + return combined; } @@ -966,6 +995,7 @@ export const extractPrimaryKeysAndValues = ( ): any => { const primaryKeysAndValues = {}; keyFields.forEach(key => (primaryKeysAndValues[key] = model[key])); + return primaryKeysAndValues; }; @@ -1012,13 +1042,14 @@ export const establishRelationAndKeys = ( typeof fieldAttribute.type === 'object' && 'model' in fieldAttribute.type ) { - const connectionType = fieldAttribute.association!.connectionType; + const { connectionType } = fieldAttribute.association!; relationship[mKey].relationTypes.push({ fieldName: fieldAttribute.name, modelName: fieldAttribute.type.model, relationType: connectionType, - targetName: fieldAttribute.association!['targetName'], - targetNames: fieldAttribute.association!['targetNames'], + targetName: fieldAttribute.association!.targetName, + targetNames: fieldAttribute.association!.targetNames, + // eslint-disable-next-line dot-notation associatedWith: fieldAttribute.association!['associatedWith'], }); @@ -1089,13 +1120,16 @@ export const getIndex = ( src: string, ): string | undefined => { let indexName; + // eslint-disable-next-line array-callback-return rel.some((relItem: RelationType) => { if (relItem.modelName === src) { const targetNames = extractTargetNamesFromSrc(relItem); indexName = targetNames && indexNameFromKeys(targetNames); + return true; } }); + return indexName; }; @@ -1112,6 +1146,7 @@ export const getIndexFromAssociation = ( } const associationIndex = indexes.find(([idxName]) => idxName === indexName); + return associationIndex && associationIndex[0]; }; @@ -1144,6 +1179,7 @@ export const indexNameFromKeys = (keys: string[]): string => { if (idx === 0) { return cur; } + return `${prev}${IDENTIFIER_KEY_SEPARATOR}${cur}`; }, ''); }; @@ -1170,7 +1206,7 @@ export const getIndexKeys = ( return [ID]; }; -//#endregion +// #endregion /** * Determine what the managed timestamp field names are for the given model definition diff --git a/packages/datastore/tslint.json b/packages/datastore/tslint.json deleted file mode 100644 index 081fa33ae8f..00000000000 --- a/packages/datastore/tslint.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "defaultSeverity": "error", - "plugins": ["prettier"], - "extends": [], - "jsRules": {}, - "rules": { - "prefer-const": true, - "no-empty-interface": true, - "no-var-keyword": true, - "object-literal-shorthand": true, - "no-eval": true, - "space-before-function-paren": [ - true, - { - "anonymous": "never", - "named": "never" - } - ], - "no-parameter-reassignment": true, - "align": [true, "parameters"], - "no-duplicate-imports": true, - "one-variable-per-declaration": [false, "ignore-for-loop"], - "triple-equals": [true, "allow-null-check"], - "comment-format": [true, "check-space"], - "indent": [false], - "whitespace": [ - false, - "check-branch", - "check-decl", - "check-operator", - "check-preblock" - ], - "eofline": true, - "variable-name": [ - true, - "check-format", - "allow-pascal-case", - "allow-snake-case", - "allow-leading-underscore" - ], - "semicolon": [ - true, - "always", - "ignore-interfaces", - "ignore-bound-class-methods" - ] - }, - "rulesDirectory": [] -}