diff --git a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts index 3cd6083f540..b700e145536 100644 --- a/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts +++ b/packages/datastore-storage-adapter/src/SQLiteAdapter/SQLiteAdapter.ts @@ -22,6 +22,7 @@ import { ModelInstanceMetadata, ModelPredicate, NamespaceResolver, + NAMESPACES, OpType, PaginationInput, PersistentModel, @@ -40,7 +41,7 @@ export class SQLiteAdapter implements StorageAdapter { private namespaceResolver: NamespaceResolver; private modelInstanceCreator: ModelInstanceCreator; private getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor; private db: SQLiteDatabase; @@ -53,7 +54,7 @@ export class SQLiteAdapter implements StorageAdapter { namespaceResolver: NamespaceResolver, modelInstanceCreator: ModelInstanceCreator, getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor ) { @@ -105,7 +106,7 @@ export class SQLiteAdapter implements StorageAdapter { this.schema.namespaces[this.namespaceResolver(modelConstructor)] ); const connectionStoreNames = Object.values(connectedModels).map( - ({ modelName, item, instance }) => { + ({ modelName, item, instance }: any) => { return { modelName, item, instance }; } ); @@ -156,7 +157,7 @@ export class SQLiteAdapter implements StorageAdapter { } private async load( - namespaceName: string, + namespaceName: NAMESPACES, srcModelName: string, records: T[] ): Promise { @@ -232,9 +233,7 @@ export class SQLiteAdapter implements StorageAdapter { // TODO: Lazy loading break; default: - const _: never = relationType as never; throw new Error(`invalid relation type ${relationType}`); - break; } } @@ -249,7 +248,9 @@ export class SQLiteAdapter implements StorageAdapter { pagination?: PaginationInput ): Promise { const { name: tableName } = modelConstructor; - const namespaceName = this.namespaceResolver(modelConstructor); + const namespaceName = this.namespaceResolver( + modelConstructor + ) as NAMESPACES; const predicates = predicate && ModelPredicateCreator.getPredicates(predicate); @@ -328,7 +329,9 @@ export class SQLiteAdapter implements StorageAdapter { ): Promise<[T[], T[]]> { if (isModelConstructor(modelOrModelConstructor)) { const modelConstructor = modelOrModelConstructor; - const namespaceName = this.namespaceResolver(modelConstructor); + const namespaceName = this.namespaceResolver( + modelConstructor + ) as NAMESPACES; const { name: tableName } = modelConstructor; const predicates = diff --git a/packages/datastore/.gitignore b/packages/datastore/.gitignore new file mode 100644 index 00000000000..15231cfea6b --- /dev/null +++ b/packages/datastore/.gitignore @@ -0,0 +1 @@ +tsconfig.json diff --git a/packages/datastore/__tests__/AsyncStorage.ts b/packages/datastore/__tests__/AsyncStorage.ts index 5596f9d2b8b..a5f9409dddd 100644 --- a/packages/datastore/__tests__/AsyncStorage.ts +++ b/packages/datastore/__tests__/AsyncStorage.ts @@ -31,12 +31,12 @@ let Comment: PersistentModelConstructor>; let Nested: NonModelTypeConstructor>; let Post: PersistentModelConstructor>; let Person: PersistentModelConstructor>; -let PostAuthorJoin: PersistentModelConstructor>; -let PostMetadata: NonModelTypeConstructor>; +let PostAuthorJoin: PersistentModelConstructor< + InstanceType +>; +let PostMetadata: NonModelTypeConstructor< + InstanceType +>; const inmemoryMap = new Map(); @@ -49,12 +49,12 @@ jest.mock('../src/storage/adapter/InMemoryStore', () => { }; multiGet = async (keys: string[]) => { return keys.reduce( - (res, k) => (res.push([k, inmemoryMap.get(k)]), res), - [] + (res, k) => (res.push([k, inmemoryMap.get(k)!]), res), + [] as [string, string][] ); }; multiRemove = async (keys: string[]) => { - return keys.forEach(k => inmemoryMap.delete(k)); + return keys.forEach((k) => inmemoryMap.delete(k)); }; setItem = async (key: string, value: string) => { return inmemoryMap.set(key, value); @@ -75,8 +75,9 @@ jest.mock('../src/storage/adapter/InMemoryStore', () => { }; }); -jest.mock('../src/storage/adapter/getDefaultAdapter/index', () => () => - AsyncStorageAdapter +jest.mock( + '../src/storage/adapter/getDefaultAdapter/index', + () => () => AsyncStorageAdapter ); /** @@ -249,7 +250,7 @@ describe('AsyncStorage tests', () => { ); expect(get1['blogOwnerId']).toBe(owner.id); - const updated = Blog.copyOf(blog, draft => { + const updated = Blog.copyOf(blog, (draft) => { draft.name = 'Avatar: The Last Airbender'; }); @@ -272,7 +273,7 @@ describe('AsyncStorage tests', () => { await DataStore.save(blog3); const query1 = await DataStore.query(Blog); - query1.forEach(async item => { + query1.forEach(async (item) => { if (item.owner) { const resolvedOwner = await item.owner; expect(resolvedOwner).toHaveProperty('name'); @@ -299,9 +300,9 @@ describe('AsyncStorage tests', () => { await DataStore.save(c2); const q1 = await DataStore.query(Comment, c1.id); - const resolvedPost = await q1.post; + const resolvedPost = await q1!.post; - expect(resolvedPost.id).toEqual(p.id); + expect(resolvedPost!.id).toEqual(p.id); }); test('query with sort on a single field', async () => { @@ -333,7 +334,7 @@ describe('AsyncStorage tests', () => { const sortedPersons = await DataStore.query(Person, null, { page: 0, limit: 20, - sort: s => s.firstName(SortDirection.DESCENDING), + sort: (s) => s.firstName(SortDirection.DESCENDING), }); expect(sortedPersons[0].firstName).toEqual('Meow Meow'); @@ -366,11 +367,11 @@ describe('AsyncStorage tests', () => { const sortedPersons = await DataStore.query( Person, - c => c.username.ne(undefined), + (c) => c.username.ne(undefined), { page: 0, limit: 20, - sort: s => + sort: (s) => s .firstName(SortDirection.ASCENDING) .lastName(SortDirection.ASCENDING) @@ -397,14 +398,14 @@ describe('AsyncStorage tests', () => { await DataStore.save(owner2); await DataStore.save( - Blog.copyOf(blog, draft => { + Blog.copyOf(blog, (draft) => { draft; }) ); await DataStore.save(blog2); await DataStore.save(blog3); - await DataStore.delete(Blog, c => c.name('beginsWith', 'Avatar')); + await DataStore.delete(Blog, (c) => c.name('beginsWith', 'Avatar')); expect(await DataStore.query(Blog, blog.id)).toBeUndefined(); expect(await DataStore.query(Blog, blog2.id)).toBeDefined(); @@ -433,7 +434,7 @@ describe('AsyncStorage tests', () => { await DataStore.delete(Comment, c1.id); expect(await DataStore.query(Comment, c1.id)).toBeUndefined; - expect((await DataStore.query(Comment, c2.id)).id).toEqual(c2.id); + expect((await DataStore.query(Comment, c2.id))!.id).toEqual(c2.id); }); test('delete 1:M function', async () => { @@ -469,7 +470,7 @@ describe('AsyncStorage tests', () => { await DataStore.delete(Post, post.id); expect(await DataStore.query(Comment, c1.id)).toBeUndefined(); expect(await DataStore.query(Comment, c2.id)).toBeUndefined(); - expect((await DataStore.query(Comment, c3.id)).id).toEqual(c3.id); + expect((await DataStore.query(Comment, c3.id))!.id).toEqual(c3.id); expect(await DataStore.query(Post, post.id)).toBeUndefined(); }); @@ -507,8 +508,8 @@ describe('AsyncStorage tests', () => { expect(postAuthorJoins).toHaveLength(0); - await DataStore.delete(Post, c => c); - await DataStore.delete(Author, c => c); + await DataStore.delete(Post, (c) => c); + await DataStore.delete(Author, (c) => c); }); // skipping in this PR. will re-enable as part of cascading deletes work @@ -621,7 +622,7 @@ function getKeyForAsyncStorage( AsyncStorageAdapter )).db._collectionInMemoryIndex; const storeName = `${namespaceName}_${modelName}`; - const ulid = collectionInMemoryIndex.get(storeName).get(id); + const ulid = collectionInMemoryIndex.get(storeName)!.get(id); return `@AmplifyDatastore::${storeName}::Data::${ulid}::${id}`; } diff --git a/packages/datastore/__tests__/AsyncStorageAdapter.test.ts b/packages/datastore/__tests__/AsyncStorageAdapter.test.ts index eeaf7d404d5..59a6c3826fe 100644 --- a/packages/datastore/__tests__/AsyncStorageAdapter.test.ts +++ b/packages/datastore/__tests__/AsyncStorageAdapter.test.ts @@ -70,7 +70,7 @@ describe('AsyncStorageAdapter tests', () => { it('Should call getById for query by id', async () => { const result = await DataStore.query(Model, model1Id); - expect(result.field1).toEqual('Some value'); + expect(result!.field1).toEqual('Some value'); expect(spyOnGetOne).toHaveBeenCalled(); expect(spyOnGetAll).not.toHaveBeenCalled(); expect(spyOnMemory).not.toHaveBeenCalled(); @@ -163,8 +163,8 @@ describe('AsyncStorageAdapter tests', () => { let profile = await DataStore.query(Profile, profile1Id); // double-checking that both of the records exist at first - expect(user.id).toEqual(user1Id); - expect(profile.id).toEqual(profile1Id); + expect(user!.id).toEqual(user1Id); + expect(profile!.id).toEqual(profile1Id); await DataStore.delete(User, user1Id); @@ -207,8 +207,8 @@ describe('AsyncStorageAdapter tests', () => { const user1Id = savedUser.id; const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(await user.profile).toEqual(profile); + expect(user!.profileID).toEqual(profile.id); + expect(await user!.profile).toEqual(profile); }); it('should allow linking model via FK', async () => { @@ -218,8 +218,8 @@ describe('AsyncStorageAdapter tests', () => { const user1Id = savedUser.id; const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(await user.profile).toEqual(profile); + expect(user!.profileID).toEqual(profile.id); + expect(await user!.profile).toEqual(profile); }); }); }); diff --git a/packages/datastore/__tests__/DataStore.ts b/packages/datastore/__tests__/DataStore.ts index 1fd1a1d633b..587274920b5 100644 --- a/packages/datastore/__tests__/DataStore.ts +++ b/packages/datastore/__tests__/DataStore.ts @@ -13,7 +13,7 @@ import { PersistentModel, PersistentModelConstructor, } from '../src/types'; -import { Model, Post, Metadata, testSchema } from './helpers'; +import { Comment, Model, Post, Metadata, testSchema } from './helpers'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; @@ -26,13 +26,15 @@ const nameOf = (name: keyof T) => name; const expectType: (param: T) => void = () => {}; describe('DataStore observe, unmocked, with fake-indexeddb', () => { + let Comment: PersistentModelConstructor; let Model: PersistentModelConstructor; let Post: PersistentModelConstructor; beforeEach(async () => { ({ initSchema, DataStore } = require('../src/datastore/datastore')); const classes = initSchema(testSchema()); - ({ Model, Post } = classes as { + ({ Comment, Model, Post } = classes as { + Comment: PersistentModelConstructor; Model: PersistentModelConstructor; Post: PersistentModelConstructor; }); @@ -40,11 +42,12 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { }); test('subscribe to all models', async done => { - DataStore.observe().subscribe(({ element, opType, model }) => { + const sub = DataStore.observe().subscribe(({ element, opType, model }) => { expectType>(model); expectType(element); expect(opType).toEqual('INSERT'); expect(element.field1).toEqual('Smurfs'); + sub.unsubscribe(); done(); }); DataStore.save( @@ -63,14 +66,17 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { }) ); - DataStore.observe(original).subscribe(({ element, opType, model }) => { - expectType>(model); - expectType(element); - expect(opType).toEqual('UPDATE'); - expect(element.id).toEqual(original.id); - expect(element.field1).toEqual('new field 1 value'); - done(); - }); + const sub = DataStore.observe(original).subscribe( + ({ element, opType, model }) => { + expectType>(model); + expectType(element); + expect(opType).toEqual('UPDATE'); + expect(element.id).toEqual(original.id); + expect(element.field1).toEqual('new field 1 value'); + sub.unsubscribe(); + done(); + } + ); // decoy await DataStore.save( @@ -93,14 +99,17 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { }) ); - DataStore.observe(Model).subscribe(({ element, opType, model }) => { - expectType>(model); - expectType(element); - expect(opType).toEqual('UPDATE'); - expect(element.id).toEqual(original.id); - expect(element.field1).toEqual('new field 1 value'); - done(); - }); + const sub = DataStore.observe(Model).subscribe( + ({ element, opType, model }) => { + expectType>(model); + expectType(element); + expect(opType).toEqual('UPDATE'); + expect(element.id).toEqual(original.id); + expect(element.field1).toEqual('new field 1 value'); + sub.unsubscribe(); + done(); + } + ); // decoy await DataStore.save( @@ -122,16 +131,17 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { }) ); - DataStore.observe(Model, m => m.field1.contains('new field 1')).subscribe( - ({ element, opType, model }) => { - expectType>(model); - expectType(element); - expect(opType).toEqual('UPDATE'); - expect(element.id).toEqual(original.id); - expect(element.field1).toEqual('new field 1 value'); - done(); - } - ); + const sub = DataStore.observe(Model, m => + m.field1.contains('new field 1') + ).subscribe(({ element, opType, model }) => { + expectType>(model); + expectType(element); + expect(opType).toEqual('UPDATE'); + expect(element.id).toEqual(original.id); + expect(element.field1).toEqual('new field 1 value'); + sub.unsubscribe(); + done(); + }); // decoy await DataStore.save( @@ -154,16 +164,17 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { }) ); - DataStore.observe(Model, m => m.field1.contains('value')).subscribe( - ({ element, opType, model }) => { - expectType>(model); - expectType(element); - expect(opType).toEqual('DELETE'); - expect(element.id).toEqual(original.id); - expect(element.field1).toEqual('somevalue'); - done(); - } - ); + const sub = DataStore.observe(Model, m => + m.field1.contains('value') + ).subscribe(({ element, opType, model }) => { + expectType>(model); + expectType(element); + expect(opType).toEqual('DELETE'); + expect(element.id).toEqual(original.id); + expect(element.field1).toEqual('somevalue'); + sub.unsubscribe(); + done(); + }); // decoy await DataStore.save( @@ -175,6 +186,455 @@ describe('DataStore observe, unmocked, with fake-indexeddb', () => { await DataStore.delete(original); }); + + test('subscribe with belongsTo criteria', async done => { + const targetPost = await DataStore.save( + new Post({ + title: 'this is my post. hooray!', + }) + ); + + const nonTargetPost = await DataStore.save( + new Post({ + title: 'this is NOT my post. boo!', + }) + ); + + const sub = DataStore.observe(Comment, comment => + comment.post.title.eq(targetPost.title) + ).subscribe(({ element: comment, opType, model }) => { + expect(comment.content).toEqual('good comment'); + sub.unsubscribe(); + done(); + }); + + await DataStore.save( + new Comment({ + content: 'bad comment', + post: nonTargetPost, + }) + ); + + await DataStore.save( + new Comment({ + content: 'good comment', + post: targetPost, + }) + ); + }); + + test('subscribe with hasMany criteria', async done => { + // want to set up a few posts and a few "non-target" comments + // to ensure we can observe post based on a single comment that's + // somewhat "buried" alongside other comments. + + const targetPost = await DataStore.save( + new Post({ + title: 'this is my post. hooray!', + }) + ); + + const nonTargetPost = await DataStore.save( + new Post({ + title: 'this is NOT my post. boo!', + }) + ); + + await DataStore.save( + new Comment({ content: 'bad comment', post: nonTargetPost }) + ); + await DataStore.save( + new Comment({ content: 'pre good comment', post: targetPost }) + ); + + const targetComment = await DataStore.save( + new Comment({ + content: 'good comment', + post: targetPost, + }) + ); + + await DataStore.save( + new Comment({ content: 'post good comment', post: targetPost }) + ); + + const sub = DataStore.observe(Post, post => + post.comments.content.eq(targetComment.content) + ).subscribe(async ({ element: post, opType, model }) => { + expect(post.title).toEqual('expected update'); + sub.unsubscribe(); + done(); + }); + + // should not see this one come through the subscription. + await DataStore.save( + Post.copyOf(nonTargetPost, p => (p.title = 'decoy update')) + ); + + // this is the update we expect to see come through, as it has + // 'good comment' in its `comments` field. + await DataStore.save( + Post.copyOf(targetPost, p => (p.title = 'expected update')) + ); + }); +}); + +describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { + // + // ~~~~ OH HEY! ~~~~~ + // + // Remember that `observeQuery()` always issues a first snapshot from the data + // already in storage. This is naturally performed async. Because of this, + // if you insert items immediately after `observeQuery()`, some of those items + // MAY show up in the initial snapshot. (Or maybe they won't!) + // + // Many of these tests should therefore include timeouts when adding records. + // These timeouts let `observeQuery()` sneak in and grab its first snapshot + // before those records hit storage, making for predictable tests. + // + // The tests should also account for that initial, empty snapshot. + // + // Remember: Snapshots are cumulative. + // + // And Also: Be careful when saving decoy records! Calling `done()` in a + // subscription body while any `DataStore.save()`'s are outstanding WILL + // result in cryptic errors that surface in subsequent tests! + // + // ("Error: An operation was called on an object on which it is not allowed ...") + // + // ~~~~ OK. Thanks! ~~~~ + // + // (That's it) + // + + let Comment: PersistentModelConstructor; + let Post: PersistentModelConstructor; + + beforeEach(async () => { + ({ initSchema, DataStore } = require('../src/datastore/datastore')); + const classes = initSchema(testSchema()); + ({ Comment, Post } = classes as { + Comment: PersistentModelConstructor; + Post: PersistentModelConstructor; + }); + await DataStore.clear(); + + // Fully faking or mocking the sync engine would be pretty significant. + // Instead, we're going to be mocking a few sync engine methods we happen know + // `observeQuery()` depends on. + (DataStore as any).sync = { + // default to report that models are NOT synced. + // set to `true` to signal the model is synced. + // `observeQuery()` should finish up after this returns `true`. + getModelSyncedStatus: (model: any) => false, + + // not important for this testing. but unsubscribe calls this. + // so, it needs to exist. + unsubscribeConnectivity: () => {}, + }; + + // how many items to accumulate before `observeQuery()` sends the items + // to its subscriber. + (DataStore as any).syncPageSize = 10; + }); + + test('publishes preexisting local data immediately', async done => { + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe(({ items }) => { + expect(items.length).toBe(5); + for (let i = 0; i < 5; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + sub.unsubscribe(); + done(); + }); + }); + + test('publishes data saved after sync', async done => { + const expecteds = [0, 10]; + + const sub = DataStore.observeQuery(Post).subscribe(({ items }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + for (let i = 0; i < 10; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + }, 100); + }); + + test('publishes preexisting local data AND follows up with subsequent saves', async done => { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + }, 100); + }); + + test('publishes received in pages data until isSynced', async done => { + const expecteds = [5, 15, 22]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (isSynced) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 21; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + // to ensure isSynced changes after the first result set comes through + // the subscription and BEFORE the last result, we use another timeout + // to change the sync status and fire off the last save. + setTimeout(async () => { + (DataStore as any).sync.getModelSyncedStatus = model => true; + await DataStore.save( + new Post({ + title: `the post 21`, + }) + ); + }, 100); + }, 100); + }); + + test('publishes received records individually after isSynced', async done => { + const expecteds = [5, 15, 16, 17, 18]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post).subscribe(({ items }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + + // to ensure isSynced changes after the first result set comes through + // the subscription and BEFORE the last result, we use another timeout + // to change the sync status and fire off the last save. + setTimeout(async () => { + (DataStore as any).sync.getModelSyncedStatus = model => true; + for (let i = 15; i < 18; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + }, 100); + }, 100); + }); + + test('publishes preexisting local data AND follows up with subsequent saves with filters', async done => { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + await DataStore.save( + new Post({ + title: `NOT the post ${i}`, + }) + ); + } + + const sub = DataStore.observeQuery(Post, p => + p.title.beginsWith('the post') + ).subscribe(({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].title).toEqual(`the post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new Post({ + title: `NOT the post ${i}`, + }) + ); + await DataStore.save( + new Post({ + title: `the post ${i}`, + }) + ); + } + }, 100); + }); + + test('publishes with hasMany criteria', async done => { + // want to set up a few posts and a few "non-target" comments + // to ensure we can observe post based on a single comment that's + // somewhat "buried" alongside other comments. + + const expectedMessages = ['this is my post. hooray!', 'expected update']; + + const targetPost = await DataStore.save( + new Post({ + title: 'this is my post. hooray!', + }) + ); + + const nonTargetPost = await DataStore.save( + new Post({ + title: 'this is NOT my post. boo!', + }) + ); + + await DataStore.save( + new Comment({ content: 'bad comment', post: nonTargetPost }) + ); + await DataStore.save( + new Comment({ content: 'pre good comment', post: targetPost }) + ); + + const targetComment = await DataStore.save( + new Comment({ + content: 'good comment', + post: targetPost, + }) + ); + + await DataStore.save( + new Comment({ content: 'post good comment', post: targetPost }) + ); + + const sub = DataStore.observeQuery(Post, post => + post.comments.content.eq(targetComment.content) + ).subscribe(async ({ items, isSynced }) => { + expect(items.length).toBe(1); + expect(items[0].title).toEqual(expectedMessages.shift()); + if (expectedMessages.length === 0) { + sub.unsubscribe(); + done(); + } + }); + + setTimeout(async () => { + // ensure we get individual results through the subscription. + (DataStore as any).sync.getModelSyncedStatus = model => true; + + // should not see this one come through the subscription. + await DataStore.save( + Post.copyOf(nonTargetPost, p => (p.title = 'decoy update')) + ); + + // this is the update we expect to see come through, as it has + // 'good comment' in its `comments` field. + await DataStore.save( + Post.copyOf(targetPost, p => (p.title = 'expected update')) + ); + }, 100); + }); }); describe('DataStore tests', () => { @@ -374,9 +834,16 @@ describe('DataStore tests', () => { const model1 = new Model({ field1: 'something', dateCreated: new Date().toISOString(), - optionalField1: null, + + // strict mode actually forbids assigning null to optional field **as we've defined them.** + // but, for customers not using strict TS and for JS developers, we need to ensure `null` + // is accepted and handled as expected. + optionalField1: null as unknown as undefined, }); + // strictly speaking (pun intended), the signature for optional fields is `type | undefined`. + // AFAIK, customers compiling exclusively in strict mode shouldn't ever see a `null` here. + // buuut, it looks like we have been allowing `null`s in. and so we must continue ... ? expect(model1.optionalField1).toBeNull(); }); @@ -443,8 +910,8 @@ describe('DataStore tests', () => { describe('Initialization', () => { test('start is called only once', async () => { - const storage: StorageType = require('../src/storage/storage') - .ExclusiveStorage; + const storage: StorageType = + require('../src/storage/storage').ExclusiveStorage; const classes = initSchema(testSchema()); @@ -463,8 +930,8 @@ describe('DataStore tests', () => { }); test('It is initialized when observing (no query)', async () => { - const storage: StorageType = require('../src/storage/storage') - .ExclusiveStorage; + const storage: StorageType = + require('../src/storage/storage').ExclusiveStorage; const classes = initSchema(testSchema()); @@ -516,7 +983,7 @@ describe('DataStore tests', () => { init: jest.fn(), save, query, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; @@ -559,7 +1026,7 @@ describe('DataStore tests', () => { init: jest.fn(), save, query, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; @@ -612,7 +1079,7 @@ describe('DataStore tests', () => { init: jest.fn(), save, query, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; @@ -646,7 +1113,7 @@ describe('DataStore tests', () => { expect(result).toMatchObject(model); model = Model.copyOf(model, draft => { - draft.emails.push('joe@doe.com'); + draft.emails?.push('joe@doe.com'); }); result = await DataStore.save(model); @@ -692,7 +1159,7 @@ describe('DataStore tests', () => { init: jest.fn(), save, query, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; @@ -738,14 +1205,14 @@ describe('DataStore tests', () => { test('Instantiation validations', async () => { expect(() => { new Model({ - field1: undefined, + field1: undefined as unknown as string, dateCreated: new Date().toISOString(), }); }).toThrowError('Field field1 is required'); expect(() => { new Model({ - field1: null, + field1: null as unknown as string, dateCreated: new Date().toISOString(), }); }).toThrowError('Field field1 is required'); @@ -779,7 +1246,7 @@ describe('DataStore tests', () => { penNames: [], nominations: [], }), - }).metadata.tags + }).metadata?.tags ).toBeUndefined(); expect(() => { @@ -789,7 +1256,7 @@ describe('DataStore tests', () => { metadata: new Metadata({ author: 'Some author', tags: undefined, - rewards: [null], + rewards: [null as unknown as string], penNames: [], nominations: [], }), @@ -802,8 +1269,8 @@ describe('DataStore tests', () => { new Model({ field1: 'someField', dateCreated: new Date().toISOString(), - emails: null, - ips: null, + emails: null as unknown as string[], + ips: null as unknown as string[], }); }).not.toThrow(); @@ -811,7 +1278,7 @@ describe('DataStore tests', () => { new Model({ field1: 'someField', dateCreated: new Date().toISOString(), - emails: [null], + emails: [null as unknown as string], }); }).toThrowError( 'All elements in the emails array should be of type string, [null] received. ' @@ -899,7 +1366,7 @@ describe('DataStore tests', () => { tags: undefined, rewards: [], penNames: [], - nominations: null, + nominations: null as unknown as undefined, }), }); }).toThrowError('Field nominations is required'); @@ -912,7 +1379,7 @@ describe('DataStore tests', () => { author: 'Some author', tags: undefined, rewards: [], - penNames: [undefined], + penNames: [undefined as unknown as string], nominations: [], }), }); @@ -959,7 +1426,7 @@ describe('DataStore tests', () => { rewards: [], penNames: [], nominations: [], - misc: [undefined], + misc: [undefined as unknown as string], }), }); }).not.toThrow(); @@ -973,7 +1440,7 @@ describe('DataStore tests', () => { rewards: [], penNames: [], nominations: [], - misc: [undefined, null], + misc: [undefined as unknown as string, null], }), }); }).not.toThrow(); @@ -1074,7 +1541,7 @@ describe('DataStore tests', () => { save, query, delete: _delete, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; @@ -1120,7 +1587,7 @@ describe('DataStore tests', () => { }); test('Delete one returns one', async () => { - let model: Model; + let model: Model | undefined; const save = jest.fn(saved => (model = saved)); const query = jest.fn(() => [model]); const _delete = jest.fn(() => [[model], [model]]); @@ -1133,7 +1600,7 @@ describe('DataStore tests', () => { save, query, delete: _delete, - runExclusive: jest.fn(fn => fn.bind(this, _mock)()), + runExclusive: jest.fn(fn => fn.bind(global, _mock)()), }; return _mock; }); @@ -1261,8 +1728,8 @@ describe('DataStore tests', () => { }); test('one by id', async () => { const oneModelById = await DataStore.query(Model, 'someid'); - expectType(oneModelById); - expect(oneModelById.field1).toBeDefined(); + expectType(oneModelById); + expect(oneModelById?.field1).toBeDefined(); expect(oneModelById).toBeInstanceOf(Model); }); test('with criteria', async () => { @@ -1304,8 +1771,8 @@ describe('DataStore tests', () => { }); test('one by id', async () => { const oneModelById = await DataStore.query(Model, 'someid'); - expectType(oneModelById); - expect(oneModelById.field1).toBeDefined(); + expectType(oneModelById); + expect(oneModelById?.field1).toBeDefined(); expect(oneModelById).toBeInstanceOf(Model); }); test('with criteria', async () => { diff --git a/packages/datastore/__tests__/IndexedDBAdapter.test.ts b/packages/datastore/__tests__/IndexedDBAdapter.test.ts index 35b289fc111..7608f5447f9 100644 --- a/packages/datastore/__tests__/IndexedDBAdapter.test.ts +++ b/packages/datastore/__tests__/IndexedDBAdapter.test.ts @@ -58,7 +58,7 @@ describe('IndexedDBAdapter tests', () => { it('Should call getById for query by id', async () => { const result = await DataStore.query(Model, model1Id); - expect(result.field1).toEqual('Some value'); + expect(result!.field1).toEqual('Some value'); expect(spyOnGetOne).toHaveBeenCalled(); expect(spyOnGetAll).not.toHaveBeenCalled(); expect(spyOnEngine).not.toHaveBeenCalled(); @@ -142,8 +142,8 @@ describe('IndexedDBAdapter tests', () => { let profile = await DataStore.query(Profile, profile1Id); // double-checking that both of the records exist at first - expect(user.id).toEqual(user1Id); - expect(profile.id).toEqual(profile1Id); + expect(user!.id).toEqual(user1Id); + expect(profile!.id).toEqual(profile1Id); await DataStore.delete(User, user1Id); @@ -186,8 +186,8 @@ describe('IndexedDBAdapter tests', () => { const user1Id = savedUser.id; const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(await user.profile).toEqual(profile); + expect(user!.profileID).toEqual(profile.id); + expect(await user!.profile).toEqual(profile); }); it('should allow linking model via FK', async () => { @@ -197,8 +197,8 @@ describe('IndexedDBAdapter tests', () => { const user1Id = savedUser.id; const user = await DataStore.query(User, user1Id); - expect(user.profileID).toEqual(profile.id); - expect(await user.profile).toEqual(profile); + expect(user!.profileID).toEqual(profile.id); + expect(await user!.profile).toEqual(profile); }); }); }); diff --git a/packages/datastore/__tests__/Merger.test.ts b/packages/datastore/__tests__/Merger.test.ts index 028315110ba..ee1482f5586 100644 --- a/packages/datastore/__tests__/Merger.test.ts +++ b/packages/datastore/__tests__/Merger.test.ts @@ -54,7 +54,7 @@ describe('ModelMerger tests', () => { ]; await Storage.runExclusive(async storage => { - await modelMerger.mergePage(storage, Model, items); + await modelMerger.mergePage(storage, Model, items as any); }); const record = await DataStore.query(Model, modelId); @@ -93,13 +93,13 @@ describe('ModelMerger tests', () => { ]; await Storage.runExclusive(async storage => { - await modelMerger.mergePage(storage, Model, items); + await modelMerger.mergePage(storage, Model, items as any); }); const record = await DataStore.query(Model, modelId); - expect(record.field1).toEqual('Another Update'); - expect(record.optionalField1).toEqual('Optional'); + expect(record!.field1).toEqual('Another Update'); + expect(record!.optionalField1).toEqual('Optional'); }); test('create > delete > create => create', async () => { @@ -133,13 +133,13 @@ describe('ModelMerger tests', () => { ]; await Storage.runExclusive(async storage => { - await modelMerger.mergePage(storage, Model, items); + await modelMerger.mergePage(storage, Model, items as any); }); const record = await DataStore.query(Model, modelId); expect(record).not.toBeUndefined(); - expect(record.field1).toEqual('New Create with the same id'); + expect(record!.field1).toEqual('New Create with the same id'); }); }); }); diff --git a/packages/datastore/__tests__/Predicate.ts b/packages/datastore/__tests__/Predicate.ts index 4a97ca51233..8c181c9a403 100644 --- a/packages/datastore/__tests__/Predicate.ts +++ b/packages/datastore/__tests__/Predicate.ts @@ -14,6 +14,7 @@ import { } from '../src/predicates'; import { validatePredicate as flatPredicateMatches } from '../src/util'; import { schema, Author, Post, Blog, BlogOwner } from './model'; +import { AsyncCollection } from '../src'; const AuthorMeta = { builder: Author, @@ -64,7 +65,7 @@ function getStorageFake(collections) { if (!predicate) { return baseSet; } else { - const predicates = ModelPredicateCreator.getPredicates(predicate); + const predicates = ModelPredicateCreator.getPredicates(predicate)!; return baseSet.filter(item => flatPredicateMatches(item, 'and', [predicates]) ); @@ -147,7 +148,7 @@ describe('Predicates', () => { // function defineTests(f) { describe('on local properties ', () => { - const getFlatAuthorsArrayFixture = function() { + const getFlatAuthorsArrayFixture = () => { return [ 'Adam West', 'Bob Jones', @@ -592,7 +593,7 @@ describe('Predicates', () => { const owner = { id: `ownerId${name}`, name, - } as ModelOf>; + } as ModelOf; return owner; }); @@ -600,10 +601,10 @@ describe('Predicates', () => { const blog = { id: `BlogID${owner.id}`, name: `${owner.name}'s Blog`, - owner, - posts: [], + owner: Promise.resolve(owner), + posts: new AsyncCollection([]), blogOwnerId: owner.id, - } as ModelOf>; + } as ModelOf; (owner as any).blog = blog; return blog; }); @@ -615,9 +616,9 @@ describe('Predicates', () => { id: `postID${blog.id}${n}`, title: `${blog.name} post ${n}`, postBlogId: blog.id, - blog, - } as ModelOf; - blog.posts.push(post); + blog: Promise.resolve(blog), + } as unknown as ModelOf; + (blog.posts.values as any).push(post); return post; }); }) @@ -701,9 +702,8 @@ describe('Predicates', () => { }); test('can filter on child collections', async () => { - const query = predicateFor(BlogMeta).posts.title.contains( - 'Bob Jones' - ); + const query = + predicateFor(BlogMeta).posts.title.contains('Bob Jones'); const matches = await mechanism.execute>(query); expect(matches.length).toBe(1); @@ -847,9 +847,10 @@ describe('Predicates', () => { }); test('can filter 4 levels deep to match all', async () => { - const query = predicateFor( - PostMeta - ).reference.reference.reference.reference.title.contains('layer 4'); + const query = + predicateFor( + PostMeta + ).reference.reference.reference.reference.title.contains('layer 4'); const matches = await mechanism.execute>(query); expect(matches.length).toBe(20); diff --git a/packages/datastore/__tests__/graphql.ts b/packages/datastore/__tests__/graphql.ts index 0e13ccdcbf1..f383d68c473 100644 --- a/packages/datastore/__tests__/graphql.ts +++ b/packages/datastore/__tests__/graphql.ts @@ -135,7 +135,10 @@ describe('DataStore GraphQL generation', () => { graphQLOpType ); - expect(print(parse(query))).toStrictEqual(print(parse(expectedGraphQL))); + // why does it think `expectedGraphQL` is `string[] | undefined`? + expect(print(parse(query))).toStrictEqual( + print(parse(expectedGraphQL as any)) + ); } ); diff --git a/packages/datastore/__tests__/helpers.ts b/packages/datastore/__tests__/helpers.ts index 752ac56a86f..9fd2450a17c 100644 --- a/packages/datastore/__tests__/helpers.ts +++ b/packages/datastore/__tests__/helpers.ts @@ -1,10 +1,5 @@ -import { - ModelInit, - MutableModel, - Schema, - InternalSchema, - SchemaModel, -} from '../src/types'; +import { ModelInit, MutableModel, Schema, InternalSchema } from '../src/types'; +import { AsyncCollection } from '../src/datastore/datastore'; export declare class Model { public readonly id: string; @@ -37,12 +32,13 @@ export declare class Metadata { export declare class Post { public readonly id: string; public readonly title: string; + public readonly comments: AsyncCollection; } export declare class Comment { public readonly id: string; public readonly content: string; - public readonly post: Post; + public readonly post: Promise; } export declare class User { @@ -191,7 +187,7 @@ export function testSchema(): Schema { isArrayNullable: true, association: { connectionType: 'HAS_MANY', - associatedWith: 'postId', + associatedWith: 'post', }, }, }, diff --git a/packages/datastore/__tests__/indexeddb.test.ts b/packages/datastore/__tests__/indexeddb.test.ts index ea9e835e9c4..587e17ac872 100644 --- a/packages/datastore/__tests__/indexeddb.test.ts +++ b/packages/datastore/__tests__/indexeddb.test.ts @@ -291,7 +291,7 @@ describe('Indexed db storage test', () => { }); }); - test('query M:1 eager load', async () => { + test('query M:1 lazy load', async () => { const p = new Post({ title: 'Avatar', blog, @@ -304,13 +304,13 @@ describe('Indexed db storage test', () => { await DataStore.save(c2); const q1 = await DataStore.query(Comment, c1.id); - const q1Post = await q1.post; - expect(q1Post.id).toEqual(p.id); + const q1Post = await q1!.post; + expect(q1Post!.id).toEqual(p.id); }); test('query lazily HAS_ONE/BELONGS_TO with explicit Field', async (done) => { const team1 = new Team({ name: 'team' }); - const savedTeam = DataStore.save(team1); + const savedTeam = await DataStore.save(team1); const project1 = new Project({ name: 'Avatar: Last Airbender', teamID: team1.id, @@ -319,9 +319,9 @@ describe('Indexed db storage test', () => { await DataStore.save(project1); - const q1 = await DataStore.query(Project, project1.id); + const q1 = (await DataStore.query(Project, project1.id))!; q1.team.then((value) => { - expect(value.id).toEqual(team1.id); + expect(value!.id).toEqual(team1.id); done(); }); }); @@ -339,10 +339,8 @@ describe('Indexed db storage test', () => { const q1 = await DataStore.query(Album, album1.id); - const songs = await q1.songs; - expect(songs).toStrictEqual( - new AsyncCollection([savedSong1, savedSong2, savedSong3]) - ); + const songs = await q1!.songs.toArray(); + expect(songs).toStrictEqual([savedSong1, savedSong2, savedSong3]); }); test('query lazily MANY to MANY ', async () => { @@ -375,22 +373,22 @@ describe('Indexed db storage test', () => { }) ); - const q1 = await DataStore.query(Forum, f1.id); - const q2 = await DataStore.query(Editor, e1.id); - const q3 = await DataStore.query(Editor, e2.id); - const editors = await q1.editors; - const forums = await q2.forums; - const forums2 = await q3.forums; + const q1 = (await DataStore.query(Forum, f1.id))!; + const q2 = (await DataStore.query(Editor, e1.id))!; + const q3 = (await DataStore.query(Editor, e2.id))!; + const editors = await q1.editors.toArray(); + const forums = await q2.forums.toArray(); + const forums2 = await q3.forums.toArray(); - expect(editors).toStrictEqual(new AsyncCollection([f1e1, f1e2])); - expect(forums).toStrictEqual(new AsyncCollection([f1e1])); - expect(forums2).toStrictEqual(new AsyncCollection([f1e2, f2e2])); + expect(editors).toStrictEqual([f1e1, f1e2]); + expect(forums).toStrictEqual([f1e1]); + expect(forums2).toStrictEqual([f1e2, f2e2]); }); test('Memoization Test', async () => { expect.assertions(3); const team1 = new Team({ name: 'team' }); - const savedTeam = DataStore.save(team1); + const savedTeam = await DataStore.save(team1); const project1 = new Project({ name: 'Avatar: Last Airbender', teamID: team1.id, @@ -398,8 +396,8 @@ describe('Indexed db storage test', () => { }); await DataStore.save(project1); - const q1 = await DataStore.query(Project, project1.id); - const q2 = await DataStore.query(Project, project1.id); + const q1 = (await DataStore.query(Project, project1.id))!; + const q2 = (await DataStore.query(Project, project1.id))!; // Ensure that model fields are actually promises if ( @@ -435,8 +433,8 @@ describe('Indexed db storage test', () => { ); await DataStore.save(new Song({ name: 'Superstar', songID: album1.id })); - const q1 = await DataStore.query(Album, album1.id); - const q2 = await DataStore.query(Album, album1.id); + const q1 = (await DataStore.query(Album, album1.id))!; + const q2 = (await DataStore.query(Album, album1.id))!; const song = await q1.songs; const song2 = await q1.songs; @@ -605,7 +603,7 @@ describe('Indexed db storage test', () => { await DataStore.delete(Comment, c1.id); expect(await DataStore.query(Comment, c1.id)).toBeUndefined; - expect((await DataStore.query(Comment, c2.id)).id).toEqual(c2.id); + expect((await DataStore.query(Comment, c2.id))?.id).toEqual(c2.id); }); test('delete 1:M function', async () => { @@ -632,7 +630,7 @@ describe('Indexed db storage test', () => { await DataStore.delete(Post, post.id); expect(await DataStore.query(Comment, c1.id)).toBeUndefined(); expect(await DataStore.query(Comment, c2.id)).toBeUndefined(); - expect((await DataStore.query(Comment, c3.id)).id).toEqual(c3.id); + expect((await DataStore.query(Comment, c3.id))?.id).toEqual(c3.id); expect(await DataStore.query(Post, post.id)).toBeUndefined(); }); @@ -762,8 +760,8 @@ describe('AsyncCollection toArray Test', () => { const savedSong4 = await DataStore.save(song4); const songsArray = [savedSong1, savedSong2, savedSong3, savedSong4]; const q1 = await DataStore.query(Album, album1.id); - const songs = await q1.songs; - const expectedValues = []; + const songs = await q1!.songs; + const expectedValues: any[] = []; for (const num of expected) { expectedValues.push(songsArray[num]); } diff --git a/packages/datastore/__tests__/model.ts b/packages/datastore/__tests__/model.ts index d79260dc6bc..936e9541ca6 100644 --- a/packages/datastore/__tests__/model.ts +++ b/packages/datastore/__tests__/model.ts @@ -12,8 +12,8 @@ import { newSchema } from './schema'; declare class BlogModel { readonly id: string; readonly name: string; - readonly posts?: PostModel[]; - readonly owner: BlogOwnerModel; + readonly posts: AsyncCollection; + readonly owner: Promise; constructor(init: ModelInit); static copyOf( source: BlogModel, @@ -24,10 +24,10 @@ declare class BlogModel { declare class PostModel { readonly id: string; readonly title: string; - readonly blog?: BlogModel; - readonly reference?: PostModel; - readonly comments?: CommentModel[]; - readonly authors?: PostAuthorJoinModel[]; + readonly blog: Promise; + readonly reference: Promise; + readonly comments: AsyncCollection; + readonly authors: AsyncCollection; readonly metadata?: PostMetadataType; constructor(init: ModelInit); static copyOf( @@ -40,7 +40,7 @@ declare class ProjectModel { readonly id: string; readonly name?: string; readonly teamID?: string; - readonly team?: Promise; + readonly team: Promise; constructor(init: ModelInit); static copyOf( source: ProjectModel, @@ -75,7 +75,7 @@ declare class NestedType { declare class CommentModel { readonly id: string; readonly content?: string; - readonly post?: PostModel; + readonly post: Promise; constructor(init: ModelInit); static copyOf( source: CommentModel, @@ -87,8 +87,8 @@ declare class CommentModel { declare class PostAuthorJoinModel { readonly id: string; - readonly author?: AuthorModel; - readonly post?: PostModel; + readonly author: Promise; + readonly post: Promise; constructor(init: ModelInit); static copyOf( source: PostAuthorJoinModel, @@ -101,7 +101,7 @@ declare class PostAuthorJoinModel { declare class ForumModel { readonly id: string; readonly title: string; - readonly editors?: AsyncCollection; + readonly editors: AsyncCollection; constructor(init: ModelInit); static copyOf( source: ForumModel, @@ -113,8 +113,8 @@ declare class ForumModel { declare class ForumEditorJoinModel { readonly id: string; - readonly editor?: Promise; - readonly forum?: Promise; + readonly editor: Promise; + readonly forum: Promise; constructor(init: ModelInit); static copyOf( source: ForumEditorJoinModel, @@ -127,7 +127,7 @@ declare class ForumEditorJoinModel { declare class EditorModel { readonly id: string; readonly name: string; - readonly forums?: AsyncCollection; + readonly forums: AsyncCollection; constructor(init: ModelInit); static copyOf( source: EditorModel, @@ -140,7 +140,7 @@ declare class EditorModel { declare class AuthorModel { readonly id: string; readonly name: string; - readonly posts?: PostAuthorJoinModel[]; + readonly posts: AsyncCollection; constructor(init: ModelInit); static copyOf( source: AuthorModel, @@ -153,7 +153,7 @@ declare class AuthorModel { declare class BlogOwnerModel { readonly name: string; readonly id: string; - readonly blog?: BlogModel; + readonly blog: Promise; constructor(init: ModelInit); static copyOf( source: BlogOwnerModel, @@ -186,7 +186,7 @@ declare class SongModel { declare class AlbumModel { readonly id: string; readonly name: string; - readonly songs?: AsyncCollection; + readonly songs: AsyncCollection; readonly createdAt?: string; readonly updatedAt?: string; constructor(init: ModelInit); diff --git a/packages/datastore/__tests__/mutation.test.ts b/packages/datastore/__tests__/mutation.test.ts index cd597bb3591..27f92353d5f 100644 --- a/packages/datastore/__tests__/mutation.test.ts +++ b/packages/datastore/__tests__/mutation.test.ts @@ -200,7 +200,7 @@ async function instantiateMutationProcessor() { }; const storage = { - runExclusive: fn => fn(), + runExclusive: (fn) => fn(), }; const mutationProcessor = new MutationProcessor( @@ -236,8 +236,8 @@ async function createMutationEvent(model, opType): Promise { .constructor as PersistentModelConstructor; return createMutationInstanceFromModelOperation( - undefined, - undefined, + undefined!, + undefined!, opType, modelConstructor, model, @@ -254,11 +254,9 @@ const axiosError = { stack: 'Error: timeout of 0ms exceeded\n at createError (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:265622:17)\n at EventTarget.handleTimeout (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:265537:16)\n at EventTarget.dispatchEvent (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:32460:27)\n at EventTarget.setReadyState (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:31623:20)\n at EventTarget.__didCompleteResponse (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:31443:16)\n at http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:31553:47\n at RCTDeviceEventEmitter.emit (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:7202:37)\n at MessageQueue.__callFunction (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2813:31)\n at http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2545:17\n at MessageQueue.__guard (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2767:13)', config: { - url: - 'https://xxxxxxxxxxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql', + url: 'https://xxxxxxxxxxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql', method: 'post', - data: - '{"query":"mutation operation($input: UpdatePostInput!, $condition: ModelPostConditionInput) { updatePost(input: $input, condition: $condition) { id title rating status _version _lastChangedAt _deleted blog { id _deleted } }}","variables":{"input":{"id":"86e8f2c1-b002-4ff2-92a2-3dad37933477","status":"INACTIVE","_version":1},"condition":null}}', + data: '{"query":"mutation operation($input: UpdatePostInput!, $condition: ModelPostConditionInput) { updatePost(input: $input, condition: $condition) { id title rating status _version _lastChangedAt _deleted blog { id _deleted } }}","variables":{"input":{"id":"86e8f2c1-b002-4ff2-92a2-3dad37933477","status":"INACTIVE","_version":1},"condition":null}}', headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json; charset=UTF-8', diff --git a/packages/datastore/__tests__/outbox.test.ts b/packages/datastore/__tests__/outbox.test.ts index d51363bb1e7..3d3cf02f53d 100644 --- a/packages/datastore/__tests__/outbox.test.ts +++ b/packages/datastore/__tests__/outbox.test.ts @@ -51,7 +51,7 @@ describe('Outbox tests', () => { }); it('Should return the create mutation from Outbox.peek', async () => { - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { let head; while (!head) { @@ -85,7 +85,7 @@ describe('Outbox tests', () => { it('Should sync the _version from a mutation response to other items with the same `id` in the queue', async () => { const last = await DataStore.query(Model, modelId); - const updatedModel1 = Model.copyOf(last, updated => { + const updatedModel1 = Model.copyOf(last, (updated) => { updated.field1 = 'another value'; updated.dateCreated = new Date().toISOString(); }); @@ -93,7 +93,7 @@ describe('Outbox tests', () => { const mutationEvent = await createMutationEvent(updatedModel1); await outbox.enqueue(Storage, mutationEvent); - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { // this mutation is now "in progress" let head; @@ -111,14 +111,14 @@ describe('Outbox tests', () => { }); // add 2 update mutations to the queue: - const updatedModel2 = Model.copyOf(last, updated => { + const updatedModel2 = Model.copyOf(last, (updated) => { updated.field1 = 'another value2'; updated.dateCreated = new Date().toISOString(); }); await outbox.enqueue(Storage, await createMutationEvent(updatedModel2)); - const updatedModel3 = Model.copyOf(last, updated => { + const updatedModel3 = Model.copyOf(last, (updated) => { updated.field1 = 'another value3'; updated.dateCreated = new Date().toISOString(); }); @@ -146,7 +146,7 @@ describe('Outbox tests', () => { updatedAt: '2021-11-30T20:52:00.250Z', }; - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { // process mutation response, which dequeues updatedModel1 // and syncs its version to the remaining item in the mutation queue await processMutationResponse( @@ -186,7 +186,7 @@ describe('Outbox tests', () => { it('Should NOT sync the _version from a handled conflict mutation response', async () => { const last = await DataStore.query(Model, modelId); - const updatedModel1 = Model.copyOf(last, updated => { + const updatedModel1 = Model.copyOf(last, (updated) => { updated.field1 = 'another value'; updated.dateCreated = new Date().toISOString(); }); @@ -194,7 +194,7 @@ describe('Outbox tests', () => { const mutationEvent = await createMutationEvent(updatedModel1); await outbox.enqueue(Storage, mutationEvent); - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { // this mutation is now "in progress" let head; @@ -212,7 +212,7 @@ describe('Outbox tests', () => { }); // add an update mutations to the queue: - const updatedModel2 = Model.copyOf(last, updated => { + const updatedModel2 = Model.copyOf(last, (updated) => { updated.field1 = 'another value2'; updated.dateCreated = new Date().toISOString(); }); @@ -238,7 +238,7 @@ describe('Outbox tests', () => { _deleted: false, }; - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { // process mutation response, which dequeues updatedModel1 // but SHOULD NOT sync the _version, since the data in the response is different await processMutationResponse( @@ -285,7 +285,7 @@ describe('Outbox tests', () => { await outbox.enqueue(Storage, mutationEvent); - const updatedModel = Model.copyOf(newModel, updated => { + const updatedModel = Model.copyOf(newModel, (updated) => { updated.optionalField1 = optionalField1; }); @@ -293,7 +293,7 @@ describe('Outbox tests', () => { await outbox.enqueue(Storage, updateMutationEvent); - await Storage.runExclusive(async s => { + await Storage.runExclusive(async (s) => { const head = await outbox.peek(s); const headData = JSON.parse(head.data); @@ -317,7 +317,7 @@ async function instantiateOutbox(): Promise { const MutationEvent = syncClasses[ 'MutationEvent' - ] as PersistentModelConstructor; + ] as PersistentModelConstructor; await DataStore.start(); @@ -363,8 +363,8 @@ async function createMutationEvent(model): Promise { .constructor as PersistentModelConstructor; return createMutationInstanceFromModelOperation( - undefined, - undefined, + undefined!, + undefined!, opType, modelConstructor, originalElement, @@ -381,7 +381,7 @@ async function processMutationResponse( ): Promise { await outbox.dequeue(storage, record, recordOp); - const modelConstructor = Model as PersistentModelConstructor; + const modelConstructor = Model as unknown as PersistentModelConstructor; const model = modelInstanceCreator(modelConstructor, record); await merger.merge(storage, model); diff --git a/packages/datastore/__tests__/storage.test.ts b/packages/datastore/__tests__/storage.test.ts index a6a71a2c29a..445057c0de6 100644 --- a/packages/datastore/__tests__/storage.test.ts +++ b/packages/datastore/__tests__/storage.test.ts @@ -58,7 +58,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.field1 = 'edited'; }) ); @@ -91,7 +91,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.field1 = 'Some value'; }) ); @@ -120,8 +120,8 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { - draft.optionalField1 = null; + Model.copyOf(model, (draft) => { + draft.optionalField1 = null!; }) ); @@ -146,7 +146,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.optionalField1 = undefined; }) ); @@ -172,7 +172,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.emails = [...draft.emails, 'joe@doe.com']; }) ); @@ -206,8 +206,8 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { - draft.emails.push('joe@doe.com'); + Model.copyOf(model, (draft) => { + draft.emails!.push('joe@doe.com'); }) ); @@ -240,7 +240,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.field1 = 'Updated value'; // same as above. should not be included in mutation input draft.emails = ['john@doe.com', 'jane@doe.com']; @@ -270,7 +270,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { // same as above. should not result in mutation event draft.emails = ['john@doe.com', 'jane@doe.com']; }) @@ -297,8 +297,8 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { - draft.emails = null; + Model.copyOf(model, (draft) => { + draft.emails = null!; }) ); @@ -327,11 +327,11 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.metadata = { ...draft.metadata, penNames: ['bob'], - }; + } as any; }) ); @@ -370,8 +370,8 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { - draft.metadata.penNames = ['bob']; + Model.copyOf(model, (draft) => { + draft.metadata!.penNames = ['bob']; }) ); @@ -390,6 +390,94 @@ describe('Storage tests', () => { ); }); + test('allowing nested BELONGS_TO to be set', async () => { + const classes = initSchema(testSchema()); + + const { Post, Comment } = classes as { + Post: PersistentModelConstructor; + Comment: PersistentModelConstructor; + }; + + const originalPost = await DataStore.save( + new Post({ + title: 'my best post ever', + }) + ); + + const newPost = await DataStore.save( + new Post({ + title: 'oops. i mean this is my best post', + }) + ); + + const comment = await DataStore.save( + new Comment({ + content: 'your post is not that great, actually ....', + post: originalPost, + }) + ); + + await DataStore.save( + Comment.copyOf(comment, (draft) => { + draft.post = newPost; + }) + ); + + const updatedComment = await DataStore.query(Comment, comment.id); + + expect((await updatedComment!.post).title).toEqual( + 'oops. i mean this is my best post' + ); + }); + + // TODO. + // Uncomment this test when implementing cascading saves + test.skip('allowing nested HAS_MANY to be set', async () => { + const classes = initSchema(testSchema()); + + const { Post, Comment } = classes as { + Post: PersistentModelConstructor; + Comment: PersistentModelConstructor; + }; + + const post = await DataStore.save( + new Post({ + title: 'my best post ever', + }) + ); + + const comment = await DataStore.save( + new Comment({ + content: 'comment 1', + post, + }) + ); + + new Comment({ + content: 'comment 1', + post, + }); + + await DataStore.save( + Post.copyOf(post, (updated) => { + updated.comments = [ + comment, + new Comment({ + content: 'comment 2', + } as any), + ]; + }) + ); + + const test = await DataStore.query(Post, post.id); + + // might have to sort + expect((await test!.comments.toArray()).map((c) => c.content)).toEqual([ + 'comment 1', + 'comment 2', + ]); + }); + test('custom type unchanged', async () => { const classes = initSchema(testSchema()); @@ -410,7 +498,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Model.copyOf(model, draft => { + Model.copyOf(model, (draft) => { draft.field1 = 'Updated value'; draft.metadata = { author: 'some author', @@ -455,7 +543,7 @@ describe('Storage tests', () => { ); await DataStore.save( - Comment.copyOf(comment, updated => { + Comment.copyOf(comment, (updated) => { updated.post = anotherPost; }) ); @@ -494,31 +582,27 @@ describe('Storage tests', () => { // `sort` is part of the key's composite sort key. // `created` should also be included in the mutation input const updated1 = await DataStore.save( - PostComposite.copyOf(post, updated => { + PostComposite.copyOf(post, (updated) => { updated.sort = 101; }) ); // `title` is the HK, so `sort` and `created` should NOT be included in the input const updated2 = await DataStore.save( - PostComposite.copyOf(updated1, updated => { + PostComposite.copyOf(updated1, (updated) => { updated.title = 'Updated Title'; }) ); // `description` does not belong to a key. No other fields should be included await DataStore.save( - PostComposite.copyOf(updated2, updated => { + PostComposite.copyOf(updated2, (updated) => { updated.description = 'Updated Desc'; }) ); - const [ - , - [postUpdate1], - [postUpdate2], - [postUpdate3], - ] = zenNext.mock.calls; + const [, [postUpdate1], [postUpdate2], [postUpdate3]] = + zenNext.mock.calls; expect(postUpdate1.element.title).toBeUndefined(); expect(postUpdate1.element.created).toEqual(createdTimestamp); @@ -554,7 +638,7 @@ describe('Storage tests', () => { ); await DataStore.save( - PostCustomPK.copyOf(post, updated => { + PostCustomPK.copyOf(post, (updated) => { updated.title = 'Updated'; }) ); @@ -584,7 +668,7 @@ describe('Storage tests', () => { ); await DataStore.save( - PostCustomPKSort.copyOf(post, updated => { + PostCustomPKSort.copyOf(post, (updated) => { updated.title = 'Updated'; }) ); @@ -602,9 +686,7 @@ describe('Storage tests', () => { // model has a custom pk (hk + composite key) defined via @key(fields: ["id", "postId", "sort"]) // all of the fields in the PK should always be included in the mutation input const { PostCustomPKComposite } = classes as { - PostCustomPKComposite: PersistentModelConstructor< - PostCustomPKComposite - >; + PostCustomPKComposite: PersistentModelConstructor; }; const post = await DataStore.save( @@ -617,7 +699,7 @@ describe('Storage tests', () => { ); await DataStore.save( - PostCustomPKComposite.copyOf(post, updated => { + PostCustomPKComposite.copyOf(post, (updated) => { updated.title = 'Updated'; }) ); diff --git a/packages/datastore/__tests__/util.test.ts b/packages/datastore/__tests__/util.test.ts index 1a5ee851f4d..f9a7f22ae9a 100644 --- a/packages/datastore/__tests__/util.test.ts +++ b/packages/datastore/__tests__/util.test.ts @@ -156,7 +156,7 @@ describe('datastore util', () => { }, ]; - let expected = []; + let expected: Set[] = []; expect(processCompositeKeys(attributes)).toEqual(expected); @@ -389,10 +389,10 @@ describe('datastore util', () => { // '2021-99-99', // '2021-01-21+99:02' ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSDate(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSDate(test)).toBe(false); }); }); @@ -422,10 +422,10 @@ describe('datastore util', () => { '12:30:.500Z', '12:30:24.500+5:30:00', ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSTime(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSTime(test)).toBe(false); }); }); @@ -460,10 +460,10 @@ describe('datastore util', () => { '2021-01-11T1:3', '20211-01-11T12:30:.500Z', ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSDateTime(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSDateTime(test)).toBe(false); }); }); @@ -471,10 +471,10 @@ describe('datastore util', () => { test('isAWSTimestamp', () => { const valid = [0, 123, 123456, 123456789]; const invalid = [-1, -123, -123456, -1234567]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSTimestamp(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSTimestamp(test)).toBe(false); }); }); @@ -493,10 +493,10 @@ describe('datastore util', () => { 'a@ b.c', 'a@b. c', ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSEmail(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSEmail(test)).toBe(false); }); }); @@ -522,10 +522,10 @@ describe('datastore util', () => { '{‘a’: 1}', 'Unquoted string', ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSJSON(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSJSON(test)).toBe(false); }); }); @@ -533,10 +533,10 @@ describe('datastore util', () => { test('isAWSURL', () => { const valid = ['http://localhost/', 'schema://anything', 'smb://a/b/c?d=e']; const invalid = ['', '//', '//example', 'example']; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSURL(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSURL(test)).toBe(false); }); }); @@ -550,10 +550,10 @@ describe('datastore util', () => { '+44123456789', ]; const invalid = ['', '+', '+-', 'a', 'bad-number']; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSPhone(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSPhone(test)).toBe(false); }); }); @@ -585,10 +585,10 @@ describe('datastore util', () => { '::ffff:10.0.0', ' ::ffff:10.0.0', ]; - valid.forEach(test => { + valid.forEach((test) => { expect(isAWSIPAddress(test)).toBe(true); }); - invalid.forEach(test => { + invalid.forEach((test) => { expect(isAWSIPAddress(test)).toBe(false); }); }); diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 512bd7279fd..17db5d35f0c 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -43,6 +43,7 @@ "devDependencies": { "@react-native-community/netinfo": "4.7.0", "@types/uuid": "3.4.5", + "@types/uuid-validate": "^0.0.1", "dexie": "3.2.0", "dexie-export-import": "1.0.3", "fake-indexeddb": "3.0.0" @@ -72,9 +73,11 @@ "esnext.asynciterable", "es2019" ], + "target": "es5", "allowJs": true, "esModuleInterop": true, - "downlevelIteration": true + "downlevelIteration": true, + "strictNullChecks": true } } }, diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index 0d601daa161..277d6879bbf 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -18,7 +18,7 @@ function getProviderFromRule( if (rule.allow === 'public' && !rule.provider) { return ModelAttributeAuthProvider.API_KEY; } - return rule.provider; + return rule.provider!; } function sortAuthRulesWithPriority(rules: ModelAttributeAuthProperty[]) { @@ -62,7 +62,7 @@ function getAuthRules({ // Using Set to ensure uniqueness const authModes = new Set(); - rules.forEach(rule => { + rules.forEach((rule) => { switch (rule.allow) { case ModelAttributeAuthAllow.CUSTOM: // custom with no provider -> function @@ -135,7 +135,7 @@ export const multiAuthStrategy: AuthModeStrategy = async ({ const { attributes } = schema.namespaces.user.models[modelName]; if (attributes) { - const authAttribute = attributes.find(attr => attr.type === 'auth'); + const authAttribute = attributes.find((attr) => attr.type === 'auth')!; if (authAttribute.properties && authAttribute.properties.rules) { const sortedRules = sortAuthRulesWithPriority( diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 463aa3d7a10..335dc635bc8 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -30,6 +30,7 @@ import { ModelInit, ModelInstanceMetadata, ModelPredicate, + ModelField, SortPredicate, MutableModel, NamespaceResolver, @@ -59,7 +60,6 @@ import { import { DATASTORE, establishRelationAndKeys, - exhaustiveCheck, isModelConstructor, monotonicUlidFactory, NAMESPACES, @@ -116,8 +116,9 @@ const getModelDefinition = ( modelConstructor: PersistentModelConstructor ) => { const namespace = modelNamespaceMap.get(modelConstructor); - - return schema.namespaces[namespace].models[modelConstructor.name]; + return namespace + ? schema.namespaces[namespace].models[modelConstructor.name] + : undefined; }; const getModelPKFieldName = ( @@ -125,9 +126,9 @@ const getModelPKFieldName = ( ) => { const namespace = modelNamespaceMap.get(modelConstructor); return ( - schema.namespaces[namespace].keys[modelConstructor.name].primaryKey || [ - 'id', - ] + (namespace && + schema.namespaces?.[namespace]?.keys?.[modelConstructor.name] + .primaryKey) || ['id'] ); }; @@ -137,8 +138,15 @@ const isValidModelConstructor = ( return isModelConstructor(obj) && modelNamespaceMap.has(obj); }; -const namespaceResolver: NamespaceResolver = modelConstructor => - modelNamespaceMap.get(modelConstructor); +const namespaceResolver: NamespaceResolver = modelConstructor => { + const resolver = modelNamespaceMap.get(modelConstructor); + if (!resolver) { + throw new Error( + `Namespace Resolver for '${modelConstructor.name}' not found! This is probably a bug in '@amplify-js/datastore'.` + ); + } + return resolver; +}; // exporting syncClasses for testing outbox.test.ts export let syncClasses: TypeConstructorMap; @@ -255,7 +263,7 @@ const initSchema = (userSchema: Schema) => { for (const modelName of Array.from(modelAssociations.keys())) { const parents = modelAssociations.get(modelName); - if (parents.every(x => result.has(x))) { + if (parents?.every(x => result.has(x))) { result.set(modelName, parents); } } @@ -391,7 +399,7 @@ const validateModelFields = } else if ( !isNullOrUndefined(v) && validateScalar && - !validateScalar(v) + !validateScalar(v as never) // TODO: why never, TS ... why ... ) { throw new Error( `Field ${name} should be of type ${type}, validation failed. ${v}` @@ -554,7 +562,7 @@ const createModelClass = ( type, association, association: { targetName }, - } = modelDefinition.fields[field]; + } = modelDefinition.fields[field] as Required; const relatedModelName = type['model']; Object.defineProperty(clazz.prototype, modelDefinition.fields[field].name, { @@ -588,12 +596,12 @@ const createModelClass = ( this[targetName] = model.id; } }, - async get() { + get() { const instanceMemos = modelInstanceAssociationsMap.get(this) || {}; - if (instanceMemos.hasOwnProperty(targetName)) { + if (targetName && instanceMemos.hasOwnProperty(targetName)) { return instanceMemos[targetName]; } - const associatedId = this[targetName]; + const associatedId = this[targetName ?? ''] as string; if (!associatedId) { if (association.connectionType === 'HAS_MANY') { @@ -606,26 +614,26 @@ const createModelClass = ( > = getModelConstructorByModelName(USER, relatedModelName); const relatedModelDefinition = getModelDefinition(relatedModel); if ( - relatedModelDefinition.fields[associatedWith].type.hasOwnProperty( - 'model' - ) + relatedModelDefinition?.fields[ + associatedWith + ].type.hasOwnProperty('model') ) { - const result = await instance.query(relatedModel, c => + const resultPromise = instance.query(relatedModel, c => c[associatedWith].id.eq(this.id) ); - const asyncResult = new AsyncCollection(result); + const asyncResult = new AsyncCollection(resultPromise); instanceMemos[field] = asyncResult; return asyncResult; } - const result = await instance.query(relatedModel, c => + const resultPromise = instance.query(relatedModel, c => c[associatedWith].eq(this.id) ); - const asyncResult = new AsyncCollection(result); + const asyncResult = new AsyncCollection(resultPromise); instanceMemos[field] = asyncResult; return asyncResult; } // unable to load related model - instanceMemos[targetName] = undefined; + targetName && (instanceMemos[targetName] = undefined); return; } @@ -634,10 +642,10 @@ const createModelClass = ( relatedModelName ); - const result = await instance.query(relatedModel, associatedId); - instanceMemos[targetName] = result; + const resultPromise = instance.query(relatedModel, associatedId); + targetName && (instanceMemos[targetName] = resultPromise); modelInstanceAssociationsMap.set(this, instanceMemos); - return result; + return resultPromise; }, }); } @@ -646,21 +654,21 @@ const createModelClass = ( }; export class AsyncCollection implements AsyncIterable { - start: number; - end: number; - values: Array; - constructor(values: Array) { - this.start = 0; - this.end = values.length; + values: Array | Promise>; + + constructor(values: Array | Promise>) { this.values = values; } + [Symbol.asyncIterator](): AsyncIterator { - let index = this.start; + let values; + let index = 0; return { next: async () => { - if (index < this.end) { + if (!values) values = await this.values; + if (index < values.length) { const result = { - value: this.values[index], + value: values[index], done: false, }; index++; @@ -673,10 +681,11 @@ export class AsyncCollection implements AsyncIterable { }, }; } + async toArray({ max = Number.MAX_SAFE_INTEGER, }: { max?: number } = {}): Promise { - const output = []; + const output: T[] = []; let i = 0; for await (const element of this) { if (i < max) { @@ -777,8 +786,7 @@ function getModelConstructorByModelName( result = storageClasses[modelName]; break; default: - exhaustiveCheck(namespaceName); - break; + throw new Error(`Invalid namespace: ${namespaceName}`); } if (isValidModelConstructor(result)) { @@ -871,23 +879,26 @@ function getNamespace(): SchemaNamespace { } class DataStore { + // Non-null assertions (bang operator) have been added to most of these properties + // to make TS happy. These properties are all expected to be set immediately after + // construction. private amplifyConfig: Record = {}; - private authModeStrategy: AuthModeStrategy; - private conflictHandler: ConflictHandler; - private errorHandler: (error: SyncError) => void; - private fullSyncInterval: number; - private initialized: Promise; - private initReject: Function; - private initResolve: Function; - private maxRecordsToSync: number; - private storage: Storage; - private sync: SyncEngine; - private syncPageSize: number; - private syncExpressions: SyncExpression[]; + private authModeStrategy!: AuthModeStrategy; + private conflictHandler!: ConflictHandler; + private errorHandler!: (error: SyncError) => void; + private fullSyncInterval!: number; + private initialized?: Promise; + private initReject!: Function; + private initResolve!: Function; + private maxRecordsToSync!: number; + private storage?: Storage; + private sync?: SyncEngine; + private syncPageSize!: number; + private syncExpressions!: SyncExpression[]; private syncPredicates: WeakMap> = new WeakMap>(); - private sessionId: string; - private storageAdapter: Adapter; + private sessionId?: string; + private storageAdapter!: Adapter; getModuleName() { return 'DataStore'; @@ -987,7 +998,7 @@ class DataStore { ): Promise; ( modelConstructor: PersistentModelConstructor, - criteria?: SingularModelPredicateExtender | typeof PredicateAll, + criteria?: SingularModelPredicateExtender | typeof PredicateAll | null, paginationProducer?: ProducerPaginationInput ): Promise; } = async ( @@ -995,11 +1006,16 @@ class DataStore { idOrCriteria?: | string | SingularModelPredicateExtender - | typeof PredicateAll, + | typeof PredicateAll + | null, paginationProducer?: ProducerPaginationInput ): Promise => { await this.start(); + if (!this.storage) { + throw new Error('No storage to query'); + } + if (!isValidModelConstructor(modelConstructor)) { const msg = 'Constructor is not for a valid model'; logger.error(msg, { modelConstructor }); @@ -1014,6 +1030,10 @@ class DataStore { } const modelDefinition = getModelDefinition(modelConstructor); + if (!modelDefinition) { + throw new Error('Invalid model definition provided!'); + } + let result: T[]; const pagination = this.processPagination( @@ -1031,7 +1051,7 @@ class DataStore { // WARNING: this conditional does not recognize Predicates.ALL ... if (!idOrCriteria || isPredicatesAll(idOrCriteria)) { // Predicates.ALL means "all records", so no predicate (undefined) - result = await this.storage.query( + result = await this.storage?.query( modelConstructor, undefined, pagination @@ -1059,11 +1079,15 @@ class DataStore { ): Promise => { await this.start(); + if (!this.storage) { + throw new Error('No storage to save to'); + } + // Immer patches for constructing a correct update mutation input // Allows us to only include changed fields for updates const patchesTuple = modelPatchesMap.get(model); - const modelConstructor: PersistentModelConstructor = model + const modelConstructor = model ? >model.constructor : undefined; @@ -1075,6 +1099,9 @@ class DataStore { } const modelDefinition = getModelDefinition(modelConstructor); + if (!modelDefinition) { + throw new Error('Model Definition could not be found for model'); + } const producedCondition = ModelPredicateCreator.createFromExisting( modelDefinition, @@ -1090,7 +1117,7 @@ class DataStore { ); }); - return savedModel; + return savedModel as T; }; setConflictHandler = (config: DataStoreConfig): ConflictHandler => { @@ -1144,7 +1171,11 @@ class DataStore { ) => { await this.start(); - let condition: ModelPredicate; + if (!this.storage) { + throw new Error('No storage to delete from'); + } + + let condition: ModelPredicate | undefined; if (!modelOrConstructor) { const msg = 'Model or Model Constructor required'; @@ -1154,7 +1185,15 @@ class DataStore { } if (isValidModelConstructor(modelOrConstructor)) { - const modelConstructor = modelOrConstructor; + const modelConstructor = + modelOrConstructor as PersistentModelConstructor; + const modelDefinition = getModelDefinition(modelConstructor); + + if (!modelDefinition) { + throw new Error( + 'Could not find model definition for modelConstructor.' + ); + } if (!idOrCriteria) { const msg = @@ -1166,12 +1205,12 @@ class DataStore { if (typeof idOrCriteria === 'string') { condition = ModelPredicateCreator.createForId( - getModelDefinition(modelConstructor), + modelDefinition, idOrCriteria ); } else { condition = ModelPredicateCreator.createFromExisting( - getModelDefinition(modelConstructor), + modelDefinition, /** * idOrCriteria is always a ProducerModelPredicate, never a symbol. * The symbol is used only for typing purposes. e.g. see Predicates.ALL @@ -1189,10 +1228,9 @@ class DataStore { } const [deleted] = await this.storage.delete(modelConstructor, condition); - return deleted; } else { - const model = modelOrConstructor; + const model = modelOrConstructor as T; const modelConstructor = Object.getPrototypeOf(model || {}) .constructor as PersistentModelConstructor; @@ -1204,6 +1242,9 @@ class DataStore { } const modelDefinition = getModelDefinition(modelConstructor); + if (!modelDefinition) { + throw new Error('Could not find model definition for model.'); + } const idPredicate = ModelPredicateCreator.createForId( modelDefinition, @@ -1285,12 +1326,25 @@ class DataStore { throw new Error(msg); } - const buildSeedPredicate = () => - predicateFor({ - builder: modelOrConstructor as PersistentModelConstructor, - schema: getModelDefinition(modelConstructor), - pkField: getModelPKFieldName(modelConstructor), + const buildSeedPredicate = () => { + if (!modelConstructor) throw new Error('Missing modelConstructor'); + + const modelSchema = getModelDefinition( + modelConstructor as PersistentModelConstructor + ); + if (!modelSchema) throw new Error('Missing modelSchema'); + + const pks = getModelPKFieldName( + modelConstructor as PersistentModelConstructor + ); + if (!pks) throw new Error('Could not determine PK'); + + return predicateFor({ + builder: modelConstructor as PersistentModelConstructor, + schema: modelSchema, + pkField: pks, }); + }; if (typeof idOrCriteria === 'string') { const buildIdPredicate = seed => seed.id.eq(idOrCriteria); @@ -1307,16 +1361,33 @@ class DataStore { (async () => { await this.start(); + if (!this.storage) { + throw new Error('No storage to query'); + } + source = this.storage .observe(modelConstructor) .filter(({ model }) => namespaceResolver(model) === USER) .subscribe({ next: async item => { + // the `element` for UPDATE events isn't an instance of `modelConstructor`. + // however, `executivePredicate` expects an instance that supports lazy loaded + // associations. customers will presumably expect the same! + let message = item; + if ( + isModelConstructor(modelConstructor) && + !(item.element instanceof modelConstructor) + ) { + message = { + ...message, + element: modelInstanceCreator(modelConstructor, item.element), + }; + } if ( !executivePredicate || - (await executivePredicate.matches(item.element)) + (await executivePredicate.matches(message.element)) ) { - observer.next(item as SubscriptionMessage); + observer.next(message as SubscriptionMessage); } }, error: err => observer.error(err), @@ -1365,49 +1436,6 @@ class DataStore { const { sort } = options || {}; const sortOptions = sort ? { sort } : undefined; - (async () => { - try { - // first, query and return any locally-available records - (await this.query(model, criteria, options)).forEach(item => - items.set(item.id, item) - ); - - // observe the model and send a stream of updates (debounced) - handle = this.observe( - model, - // @ts-ignore TODO: fix this TSlint error - criteria - ).subscribe(({ element, model, opType }) => { - // Flag items which have been recently deleted - // NOTE: Merging of separate operations to the same model instance is handled upstream - // in the `mergePage` method within src/sync/merger.ts. The final state of a model instance - // depends on the LATEST record (for a given id). - if (opType === 'DELETE') { - deletedItemIds.push(element.id); - } else { - itemsChanged.set(element.id, element); - } - - const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; - - const limit = - itemsChanged.size - deletedItemIds.length >= this.syncPageSize; - - if (limit || isSynced) { - limitTimerRace.resolve(); - } - - // kicks off every subsequent race as results sync down - limitTimerRace.start(); - }); - - // returns a set of initial/locally-available results - generateAndEmitSnapshot(); - } catch (err) { - observer.error(err); - } - })(); - // TODO: abstract this function into a util file to be able to write better unit tests const generateSnapshot = (): DataStoreSnapshot => { const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; @@ -1443,7 +1471,12 @@ class DataStore { const sortItems = (itemsToSort: T[]): void => { const modelDefinition = getModelDefinition(model); + if (!modelDefinition) throw new Error('Missing model definition'); + const pagination = this.processPagination(modelDefinition, options); + if (!(pagination && pagination.sort)) { + throw new Error('Pagination could not be processed'); + } const sortPredicates = ModelSortPredicateCreator.getPredicates( pagination.sort @@ -1468,6 +1501,46 @@ class DataStore { }; Hub.listen('datastore', hubCallback); + (async () => { + try { + // first, query and return any locally-available records + (await this.query(model, criteria, options)).forEach(item => + items.set(item.id, item) + ); + + // observe the model and send a stream of updates (debounced) + handle = this.observe( + model, + // @ts-ignore TODO: fix this TSlint error + criteria + ).subscribe(({ element, model, opType }) => { + // Flag items which have been recently deleted + // NOTE: Merging of separate operations to the same model instance is handled upstream + // in the `mergePage` method within src/sync/merger.ts. The final state of a model instance + // depends on the LATEST record (for a given id). + if (opType === 'DELETE') { + deletedItemIds.push(element.id); + } else { + itemsChanged.set(element.id, element); + } + + const isSynced = this.sync?.getModelSyncedStatus(model) ?? false; + + if ( + itemsChanged.size - deletedItemIds.length >= this.syncPageSize || + isSynced + ) { + generateAndEmitSnapshot(); + } + }); + + // will return any locally-available items in the first snapshot + generateAndEmitSnapshot(); + } catch (err) { + observer.error(err); + } + })(); + return () => { if (handle) { handle.unsubscribe(); @@ -1558,7 +1631,7 @@ class DataStore { this.sessionId = this.retrieveSessionId(); }; - clear = async function clear() { + clear = async () => { if (this.storage === undefined) { return; } @@ -1579,7 +1652,7 @@ class DataStore { this.syncPredicates = new WeakMap>(); }; - stop = async function stop() { + stop = async () => { if (this.initialized !== undefined) { await this.start(); } @@ -1598,9 +1671,9 @@ class DataStore { private processPagination( modelDefinition: SchemaModel, - paginationProducer: ProducerPaginationInput + paginationProducer?: ProducerPaginationInput ): PaginationInput | undefined { - let sortPredicate: SortPredicate; + let sortPredicate: SortPredicate | undefined; const { limit, page, sort } = paginationProducer || {}; if (limit === undefined && page === undefined && sort === undefined) { @@ -1634,7 +1707,7 @@ class DataStore { if (sort) { sortPredicate = ModelSortPredicateCreator.createFromExisting( modelDefinition, - paginationProducer.sort + sort ); } @@ -1664,15 +1737,15 @@ class DataStore { // OR a function/promise that returns a predicate const condition = await this.unwrapPromise(conditionProducer); if (isPredicatesAll(condition)) { - return [modelDefinition, null]; + return [modelDefinition as any, null as any]; } const predicate = this.createFromCondition( - modelDefinition, + modelDefinition as any, condition ); - return [modelDefinition, predicate]; + return [modelDefinition as any, predicate as any]; } ) ); diff --git a/packages/datastore/src/index.ts b/packages/datastore/src/index.ts index 670fb0300f5..86738d15017 100644 --- a/packages/datastore/src/index.ts +++ b/packages/datastore/src/index.ts @@ -21,6 +21,8 @@ import { isModelConstructor, } from './util'; +export { NAMESPACES } from './util'; + export const utils = { USER, traverseModel, diff --git a/packages/datastore/src/predicates/index.ts b/packages/datastore/src/predicates/index.ts index 09c5f331179..c00a55d987a 100644 --- a/packages/datastore/src/predicates/index.ts +++ b/packages/datastore/src/predicates/index.ts @@ -8,7 +8,6 @@ import { ProducerModelPredicate, SchemaModel, } from '../types'; -import { exhaustiveCheck } from '../util'; export { ModelSortPredicateCreator } from './sort'; @@ -25,7 +24,7 @@ export const PredicateAll = Symbol('A predicate that matches all records'); export class Predicates { public static get ALL(): typeof PredicateAll { - const predicate = >(c => c); + const predicate = >((c) => c); predicatesAllSet.add(predicate); @@ -44,7 +43,7 @@ export class ModelPredicateCreator { ) { const { name: modelName } = modelDefinition; const fieldNames = new Set(Object.keys(modelDefinition.fields)); - Object.values(modelDefinition.fields).forEach(field => { + Object.values(modelDefinition.fields).forEach((field) => { if (field.association) { if (field.association.targetName) { fieldNames.add(field.association.targetName); @@ -83,7 +82,7 @@ export class ModelPredicateCreator { // Set the recorder group ModelPredicateCreator.predicateGroupsMap.set( - tmpPredicateRecorder, + tmpPredicateRecorder as any, group ); @@ -92,15 +91,15 @@ export class ModelPredicateCreator { // Push the group to the top-level recorder ModelPredicateCreator.predicateGroupsMap - .get(receiver) - .predicates.push(group); + .get(receiver as any) + ?.predicates.push(group); return receiver; }; return result; default: - exhaustiveCheck(groupType, false); + // intentionally blank. } const field = propertyKey as keyof T; @@ -116,8 +115,8 @@ export class ModelPredicateCreator { operand: any ) => { ModelPredicateCreator.predicateGroupsMap - .get(receiver) - .predicates.push({ field, operator, operand }); + .get(receiver as any) + ?.predicates.push({ field, operator, operand }); return receiver; }; return result; @@ -129,7 +128,7 @@ export class ModelPredicateCreator { type: 'and', predicates: [], }; - ModelPredicateCreator.predicateGroupsMap.set(predicate, group); + ModelPredicateCreator.predicateGroupsMap.set(predicate as any, group); return predicate; } @@ -148,13 +147,13 @@ export class ModelPredicateCreator { throw new Error('The predicate is not valid'); } - return ModelPredicateCreator.predicateGroupsMap.get(predicate); + return ModelPredicateCreator.predicateGroupsMap.get(predicate as any); } // transforms cb-style predicate into Proxy static createFromExisting( - modelDefinition: SchemaModel, - existing: ProducerModelPredicate + modelDefinition?: SchemaModel, + existing?: ProducerModelPredicate ) { if (!existing || !modelDefinition) { return undefined; diff --git a/packages/datastore/src/predicates/next.ts b/packages/datastore/src/predicates/next.ts index ee46868f01f..eb82fed02b2 100644 --- a/packages/datastore/src/predicates/next.ts +++ b/packages/datastore/src/predicates/next.ts @@ -4,6 +4,7 @@ import { ModelFieldType, ModelMeta, AllOperators, + PredicateFieldType, } from '../types'; import { ModelPredicateCreator as FlatModelPredicateCreator } from './index'; @@ -20,20 +21,6 @@ type MatchableTypes = type AllFieldOperators = keyof AllOperators; -type AsyncCollection = { - toArray(): T[]; -}; - -type FinalFieldType = NonNullable< - Scalar< - T extends Promise - ? InnerPromiseType - : T extends AsyncCollection - ? InnerCollectionType - : T - > ->; - const ops: AllFieldOperators[] = [ 'eq', 'ne', @@ -92,9 +79,9 @@ type ModelPredicateNegation = ( predicate: SingularModelPredicateExtender | FinalModelPredicate ) => FinalModelPredicate; -type ModelPredicate = { - [K in keyof RT]-?: FinalFieldType extends PersistentModel - ? ModelPredicate> +export type ModelPredicate = { + [K in keyof RT]-?: PredicateFieldType extends PersistentModel + ? ModelPredicate> : ValuePredicate; } & { or: ModelPredicateOperator; @@ -149,7 +136,7 @@ function applyConditionsToV1Predicate( negateChildren: boolean ): T { let p = predicate; - const finalConditions = []; + const finalConditions: FieldCondition[] = []; for (const c of conditions) { if (negateChildren) { @@ -250,7 +237,7 @@ export class FieldCondition { * Throws an exception if the `count` disagrees with `operands.length`. * @param count The number of `operands` expected. */ - const argumentCount = count => { + const argumentCount = (count) => { const argsClause = count === 1 ? 'argument is' : 'arguments are'; return () => { if (this.operands.length !== count) { @@ -340,7 +327,7 @@ export class GroupCondition { let extractedCopy: GroupCondition | undefined = extract === this ? copied : undefined; - this.operands.forEach(o => { + this.operands.forEach((o) => { const [operandCopy, extractedFromOperand] = o.copy(extract); copied.operands.push(operandCopy); extractedCopy = extractedCopy || extractedFromOperand; @@ -359,7 +346,7 @@ export class GroupCondition { */ async fetch( storage: StorageAdapter, - breadcrumb = [], + breadcrumb: string[] = [], negate = false ): Promise[]> { const resultGroups: Array[]> = []; @@ -372,11 +359,11 @@ export class GroupCondition { const negateChildren = negate !== (this.operator === 'not'); const groups = this.operands.filter( - op => op instanceof GroupCondition + (op) => op instanceof GroupCondition ) as GroupCondition[]; const conditions = this.operands.filter( - op => op instanceof FieldCondition + (op) => op instanceof FieldCondition ) as FieldCondition[]; // TODO: fetch Predicate.ALL return early here? @@ -436,7 +423,7 @@ export class GroupCondition { rightHandField = 'id'; } - const joinConditions = []; + const joinConditions: FieldCondition[] = []; for (const relative of relatives) { // await right-hand value, b/c it will eventually be lazy-loaded in some cases. const rightHandValue = @@ -448,8 +435,8 @@ export class GroupCondition { const predicate = FlatModelPredicateCreator.createFromExisting( this.model.schema, - p => - p.or(inner => + (p) => + p.or((inner) => applyConditionsToV1Predicate( inner, joinConditions, @@ -475,8 +462,8 @@ export class GroupCondition { if (conditions.length > 0) { const predicate = FlatModelPredicateCreator.createFromExisting( this.model.schema, - p => - p[operator](c => + (p) => + p[operator]((c) => applyConditionsToV1Predicate(c, conditions, negateChildren) ) ); @@ -490,11 +477,11 @@ export class GroupCondition { // PK might be a single field, like `id`, or it might be several fields. // so, we'll need to extract the list of PK fields from an object // and stringify the list it for easy comparision / merging. - const getPKValue = item => - JSON.stringify(this.model.pkField.map(name => item[name])); + const getPKValue = (item) => + JSON.stringify(this.model.pkField.map((name) => item[name])); // will be used for intersecting or unioning results - let resultIndex: Map>; + let resultIndex: Map> | undefined; if (operator === 'and') { if (resultGroups.length === 0) { @@ -505,10 +492,10 @@ export class GroupCondition { // that aren't present in each subsequent group. for (const group of resultGroups) { if (resultIndex === undefined) { - resultIndex = new Map(group.map(item => [getPKValue(item), item])); + resultIndex = new Map(group.map((item) => [getPKValue(item), item])); } else { const intersectWith = new Map>( - group.map(item => [getPKValue(item), item]) + group.map((item) => [getPKValue(item), item]) ); for (const k of resultIndex.keys()) { if (!intersectWith.has(k)) { @@ -532,7 +519,7 @@ export class GroupCondition { } } - return Array.from(resultIndex.values()); + return Array.from(resultIndex?.values() || []); } /** @@ -558,8 +545,11 @@ export class GroupCondition { return false; } - if (this.relationshipType === 'HAS_MANY' && itemToCheck instanceof Array) { - for (const singleItem of itemToCheck) { + if ( + this.relationshipType === 'HAS_MANY' && + typeof itemToCheck[Symbol.asyncIterator] === 'function' + ) { + for await (const singleItem of itemToCheck) { if (await this.matches(singleItem, true)) { return true; } @@ -568,9 +558,9 @@ export class GroupCondition { } if (this.operator === 'or') { - return asyncSome(this.operands, c => c.matches(itemToCheck)); + return asyncSome(this.operands, (c) => c.matches(itemToCheck)); } else if (this.operator === 'and') { - return asyncEvery(this.operands, c => c.matches(itemToCheck)); + return asyncEvery(this.operands, (c) => c.matches(itemToCheck)); } else if (this.operator === 'not') { if (this.operands.length !== 1) { throw new Error( @@ -625,7 +615,7 @@ export function predicateFor( query?: GroupCondition, tail?: GroupCondition ): ModelPredicate { - let starter: GroupCondition; + let starter: GroupCondition | undefined; // if we don't have an existing query + tail to build onto, // we need to start a new query chain. if (!query || !tail) { @@ -641,14 +631,14 @@ export function predicateFor( const [query, newtail] = link.__query.copy(link.__tail); return predicateFor(ModelType, undefined, query, newtail); }, - filter: items => { - return asyncFilter(items, i => link.__query.matches(i)); + filter: (items) => { + return asyncFilter(items, (i) => link.__query.matches(i)); }, } as ModelPredicate; // TODO: consider a proxy // adds .or() and .and() methods to the link. - ['and', 'or'].forEach(op => { + ['and', 'or'].forEach((op) => { (link as any)[op] = ( ...builderOrPredicates: | [ModelPredicateExtender] @@ -669,10 +659,12 @@ export function predicateFor( typeof builderOrPredicates[0] === 'function' ? // handle the the `c => [c.field.eq(v)]` form builderOrPredicates[0](predicateFor(ModelType)).map( - p => p.__query + (p) => p.__query ) : // handle the `[MyModel.field.eq(v)]` form (not yet available) - (builderOrPredicates as FinalModelPredicate[]).map(p => p.__query) + (builderOrPredicates as FinalModelPredicate[]).map( + (p) => p.__query + ) ) ); @@ -680,8 +672,8 @@ export function predicateFor( return { __query: newlink.__query, __tail: newlink.__tail, - filter: items => { - return asyncFilter(items, i => newlink.__query.matches(i)); + filter: (items) => { + return asyncFilter(items, (i) => newlink.__query.matches(i)); }, }; }; @@ -718,8 +710,8 @@ export function predicateFor( return { __query: newlink.__query, __tail: newlink.__tail, - filter: items => { - return asyncFilter(items, i => newlink.__query.matches(i)); + filter: (items) => { + return asyncFilter(items, (i) => newlink.__query.matches(i)); }, }; }; @@ -763,8 +755,10 @@ export function predicateFor( return { __query: newlink.__query, __tail: newlink.__tail, - filter: items => { - return asyncFilter(items, i => newlink.__query.matches(i)); + filter: (items: any[]) => { + return asyncFilter(items, (i) => + newlink.__query.matches(i) + ); }, }; }, @@ -779,12 +773,19 @@ export function predicateFor( // the use has just typed '.someRelatedModel'. we need to given them // back a predicate chain. + const relatedMeta = (def.type as ModelFieldType).modelConstructor; + if (!relatedMeta) { + throw new Error( + 'Related model metadata is missing. This is a bug! Please report it.' + ); + } + // `Model.reletedModelField` returns a copy of the original link, // and will contains copies of internal GroupConditions // to head off mutability concerns. const [newquery, oldtail] = link.__query.copy(link.__tail); const newtail = new GroupCondition( - (def.type as ModelFieldType).modelConstructor, + relatedMeta, fieldName, def.association.connectionType, 'and', @@ -796,7 +797,7 @@ export function predicateFor( // it to push the *new* tail onto the end of it. (oldtail as GroupCondition).operands.push(newtail); const newlink = predicateFor( - (def.type as ModelFieldType).modelConstructor, + relatedMeta, undefined, newquery, newtail @@ -804,7 +805,7 @@ export function predicateFor( return newlink; } else { throw new Error( - "Oh no! Related Model definition doesn't have a typedef!" + "Related model definition doesn't have a typedef. This is a bug! Please report it." ); } } diff --git a/packages/datastore/src/predicates/sort.ts b/packages/datastore/src/predicates/sort.ts index f9c09f81f86..c5451475877 100644 --- a/packages/datastore/src/predicates/sort.ts +++ b/packages/datastore/src/predicates/sort.ts @@ -35,7 +35,7 @@ export class ModelSortPredicateCreator { const result = (sortDirection: SortDirection) => { ModelSortPredicateCreator.sortPredicateGroupsMap .get(receiver) - .push({ field, sortDirection }); + ?.push({ field, sortDirection }); return receiver; }; @@ -66,7 +66,13 @@ export class ModelSortPredicateCreator { throw new Error('The predicate is not valid'); } - return ModelSortPredicateCreator.sortPredicateGroupsMap.get(predicate); + const predicateGroup = + ModelSortPredicateCreator.sortPredicateGroupsMap.get(predicate); + if (predicateGroup) { + return predicateGroup; + } else { + throw new Error('Predicate group not found'); + } } // transforms cb-style predicate into Proxy diff --git a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts index a06a5e4a5ba..5510a2e5ad3 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts @@ -19,29 +19,33 @@ import { RelationType, } from '../../types'; import { - exhaustiveCheck, getIndex, getIndexFromAssociation, isModelConstructor, traverseModel, validatePredicate, inMemoryPagination, + NAMESPACES, } from '../../util'; const logger = new Logger('DataStore'); export class AsyncStorageAdapter implements Adapter { - private schema: InternalSchema; - private namespaceResolver: NamespaceResolver; - private modelInstanceCreator: ModelInstanceCreator; - private getModelConstructorByModelName: ( - namsespaceName: string, + // Non-null assertions (bang operators) added to most properties to make TS happy. + // For now, we can be reasonably sure they're available when they're needed, because + // the adapter is not used directly outside the library boundary. + // TODO: rejigger for DI? + private schema!: InternalSchema; + private namespaceResolver!: NamespaceResolver; + private modelInstanceCreator!: ModelInstanceCreator; + private getModelConstructorByModelName!: ( + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor; - private db: AsyncStorageDatabase; - private initPromise: Promise; - private resolve: (value?: any) => void; - private reject: (value?: any) => void; + private db!: AsyncStorageDatabase; + private initPromise!: Promise; + private resolve!: (value?: any) => void; + private reject!: (value?: any) => void; private getStorenameForModel( modelConstructor: PersistentModelConstructor @@ -63,7 +67,7 @@ export class AsyncStorageAdapter implements Adapter { namespaceResolver: NamespaceResolver, modelInstanceCreator: ModelInstanceCreator, getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor ) { @@ -116,7 +120,7 @@ export class AsyncStorageAdapter implements Adapter { if (condition && fromDB) { const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; + const { predicates: predicateObjs, type } = predicates!; const isValid = validatePredicate(fromDB, type, predicateObjs); @@ -148,12 +152,12 @@ export class AsyncStorageAdapter implements Adapter { } private async load( - namespaceName: string, + namespaceName: NAMESPACES, srcModelName: string, records: T[] ): Promise { const namespace = this.schema.namespaces[namespaceName]; - const relations = namespace.relationships[srcModelName].relationTypes; + const relations = namespace.relationships![srcModelName].relationTypes; const connectionStoreNames = relations.map(({ modelName }) => { return this.getStorename(namespaceName, modelName); }); @@ -179,7 +183,9 @@ export class AsyncStorageAdapter implements Adapter { pagination?: PaginationInput ): Promise { const storeName = this.getStorenameForModel(modelConstructor); - const namespaceName = this.namespaceResolver(modelConstructor); + const namespaceName = this.namespaceResolver( + modelConstructor + ) as NAMESPACES; const predicates = predicate && ModelPredicateCreator.getPredicates(predicate); @@ -274,14 +280,15 @@ export class AsyncStorageAdapter implements Adapter { const deleteQueue: { storeName: string; items: T[] }[] = []; if (isModelConstructor(modelOrModelConstructor)) { - const modelConstructor = modelOrModelConstructor; - const nameSpace = this.namespaceResolver(modelConstructor); + const modelConstructor = + modelOrModelConstructor as PersistentModelConstructor; + const nameSpace = this.namespaceResolver(modelConstructor) as NAMESPACES; // models to be deleted. - const models = await this.query(modelConstructor, condition); + const models = await this.query(modelConstructor, condition!); // TODO: refactor this to use a function like getRelations() const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] + this.schema.namespaces[nameSpace].relationships![modelConstructor.name] .relationTypes; if (condition !== undefined) { @@ -319,11 +326,11 @@ export class AsyncStorageAdapter implements Adapter { return [models, deletedModels]; } } else { - const model = modelOrModelConstructor; + const model = modelOrModelConstructor as T; const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor; - const nameSpace = this.namespaceResolver(modelConstructor); + const nameSpace = this.namespaceResolver(modelConstructor) as NAMESPACES; const storeName = this.getStorenameForModel(modelConstructor); if (condition) { @@ -337,7 +344,7 @@ export class AsyncStorageAdapter implements Adapter { } const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; + const { predicates: predicateObjs, type } = predicates!; const isValid = validatePredicate(fromDB, type, predicateObjs); if (!isValid) { @@ -348,8 +355,9 @@ export class AsyncStorageAdapter implements Adapter { } const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] - .relationTypes; + this.schema.namespaces[nameSpace].relationships![ + modelConstructor.name + ].relationTypes; await this.deleteTraverse( relations, [model], @@ -359,8 +367,9 @@ export class AsyncStorageAdapter implements Adapter { ); } else { const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] - .relationTypes; + this.schema.namespaces[nameSpace].relationships![ + modelConstructor.name + ].relationTypes; await this.deleteTraverse( relations, @@ -385,7 +394,7 @@ export class AsyncStorageAdapter implements Adapter { private async deleteItem( deleteQueue?: { storeName: string; items: T[] | IDBValidKey[] }[] ) { - for await (const deleteItem of deleteQueue) { + for await (const deleteItem of deleteQueue!) { const { storeName, items } = deleteItem; for await (const item of items) { @@ -398,6 +407,7 @@ export class AsyncStorageAdapter implements Adapter { } } } + /** * Populates the delete Queue with all the items to delete * @param relations @@ -410,7 +420,7 @@ export class AsyncStorageAdapter implements Adapter { relations: RelationType[], models: T[], srcModel: string, - nameSpace: string, + nameSpace: NAMESPACES, deleteQueue: { storeName: string; items: T[] }[] ): Promise { for await (const rel of relations) { @@ -419,7 +429,7 @@ export class AsyncStorageAdapter implements Adapter { const index: string = getIndex( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, srcModel ) || @@ -427,8 +437,8 @@ export class AsyncStorageAdapter implements Adapter { // i.e. for keyName connections, attempt to find one by the // associatedWith property getIndexFromAssociation( - this.schema.namespaces[nameSpace].relationships[modelName].indexes, - rel.associatedWith + this.schema.namespaces[nameSpace].relationships![modelName].indexes, + rel.associatedWith! ); switch (relationType) { @@ -436,9 +446,8 @@ export class AsyncStorageAdapter implements Adapter { for await (const model of models) { const hasOneIndex = index || 'byId'; - const hasOneCustomField = targetName in model; - const value = hasOneCustomField ? model[targetName] : model.id; - if (!value) break; + const hasOneCustomField = targetName! in model; + const value = hasOneCustomField ? model[targetName!] : model.id; const allRecords = await this.db.getAll(storeName); const recordToDelete = allRecords.filter( @@ -446,7 +455,7 @@ export class AsyncStorageAdapter implements Adapter { ); await this.deleteTraverse( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, recordToDelete, modelName, @@ -463,7 +472,7 @@ export class AsyncStorageAdapter implements Adapter { ); await this.deleteTraverse( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, childrenArray, modelName, @@ -476,8 +485,7 @@ export class AsyncStorageAdapter implements Adapter { // Intentionally blank break; default: - exhaustiveCheck(relationType); - break; + throw new Error(`Invalid relationType ${relationType}`); } } @@ -495,8 +503,8 @@ export class AsyncStorageAdapter implements Adapter { async clear(): Promise { await this.db.clear(); - this.db = undefined; - this.initPromise = undefined; + this.db = undefined!; + this.initPromise = undefined!; } async batchSave( @@ -520,7 +528,7 @@ export class AsyncStorageAdapter implements Adapter { const { instance } = connectedModels.find( ({ instance }) => instance.id === id - ); + )!; batch.push(instance); } diff --git a/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts b/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts index 4b02bbf927f..69ed1b65973 100644 --- a/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts +++ b/packages/datastore/src/storage/adapter/AsyncStorageDatabase.ts @@ -36,7 +36,7 @@ class AsyncStorageDatabase { monotonicFactoriesMap.set(storeName, monotonicUlidFactory()); } - return monotonicFactoriesMap.get(storeName); + return monotonicFactoriesMap.get(storeName)!; } async init(): Promise { @@ -44,7 +44,7 @@ class AsyncStorageDatabase { const allKeys: string[] = await this.storage.getAllKeys(); - const keysForCollectionEntries = []; + const keysForCollectionEntries: string[] = []; for (const key of allKeys) { const [dbName, storeName, recordType, ulidOrId, id] = key.split('::'); @@ -65,7 +65,7 @@ class AsyncStorageDatabase { const item = await this.storage.getItem(oldKey); - await this.storage.setItem(newKey, item); + await this.storage.setItem(newKey, item!); await this.storage.removeItem(oldKey); ulid = newUlid; @@ -73,7 +73,7 @@ class AsyncStorageDatabase { ulid = ulidOrId; } - this.getCollectionIndex(storeName).set(id, ulid); + this.getCollectionIndex(storeName)!.set(id, ulid); } else if (recordType === COLLECTION) { keysForCollectionEntries.push(key); } @@ -87,12 +87,12 @@ class AsyncStorageDatabase { async save(item: T, storeName: string) { const ulid = - this.getCollectionIndex(storeName).get(item.id) || + this.getCollectionIndex(storeName)!.get(item.id) || this.getMonotonicFactory(storeName)(); const itemKey = this.getKeyForItem(storeName, item.id, ulid); - this.getCollectionIndex(storeName).set(item.id, ulid); + this.getCollectionIndex(storeName)!.set(item.id, ulid); await this.storage.setItem(itemKey, JSON.stringify(item)); } @@ -107,11 +107,11 @@ class AsyncStorageDatabase { const result: [T, OpType][] = []; - const collection = this.getCollectionIndex(storeName); + const collection = this.getCollectionIndex(storeName)!; const keysToDelete = new Set(); const keysToSave = new Set(); - const allItemsKeys = []; + const allItemsKeys: string[] = []; const itemsMap: Record = {}; for (const item of items) { const { id, _deleted } = item; @@ -144,7 +144,7 @@ class AsyncStorageDatabase { const keysToDeleteArray = Array.from(keysToDelete); - keysToDeleteArray.forEach(key => + keysToDeleteArray.forEach((key) => collection.delete(itemsMap[key].model.id) ); @@ -163,12 +163,12 @@ class AsyncStorageDatabase { return; } - const entriesToSet = Array.from(keysToSave).map(key => [ + const entriesToSet = Array.from(keysToSave).map((key) => [ key, JSON.stringify(itemsMap[key].model), ]); - keysToSave.forEach(key => { + keysToSave.forEach((key) => { const { model: { id }, ulid, @@ -204,7 +204,7 @@ class AsyncStorageDatabase { id: string, storeName: string ): Promise { - const ulid = this.getCollectionIndex(storeName).get(id); + const ulid = this.getCollectionIndex(storeName)!.get(id)!; const itemKey = this.getKeyForItem(storeName, id, ulid); const recordAsString = await this.storage.getItem(itemKey); const record = recordAsString && JSON.parse(recordAsString); @@ -212,19 +212,19 @@ class AsyncStorageDatabase { } async getOne(firstOrLast: QueryOne, storeName: string) { - const collection = this.getCollectionIndex(storeName); + const collection = this.getCollectionIndex(storeName)!; 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]; + return [id!, ulid!]; })() : (() => { let id: string, ulid: string; for ([id, ulid] of collection); // Get last element of the set - return [id, ulid]; + return [id!, ulid!]; })(); const itemKey = this.getKeyForItem(storeName, itemId, ulid); const itemString = itemKey && (await this.storage.getItem(itemKey)); @@ -242,7 +242,7 @@ class AsyncStorageDatabase { storeName: string, pagination?: PaginationInput ): Promise { - const collection = this.getCollectionIndex(storeName); + const collection = this.getCollectionIndex(storeName)!; const { page = 0, limit = 0 } = pagination || {}; const start = Math.max(0, page * limit) || 0; @@ -273,10 +273,10 @@ class AsyncStorageDatabase { } async delete(id: string, storeName: string) { - const ulid = this.getCollectionIndex(storeName).get(id); - const itemKey = this.getKeyForItem(storeName, id, ulid); + const ulid = this.getCollectionIndex(storeName)!.get(id); + const itemKey = this.getKeyForItem(storeName, id, ulid!); - this.getCollectionIndex(storeName).delete(id); + this.getCollectionIndex(storeName)!.delete(id); await this.storage.removeItem(itemKey); } @@ -285,7 +285,7 @@ class AsyncStorageDatabase { */ async clear() { const allKeys = await this.storage.getAllKeys(); - const allDataStoreKeys = allKeys.filter(key => key.startsWith(DB_NAME)); + const allDataStoreKeys = allKeys.filter((key) => key.startsWith(DB_NAME)); await this.storage.multiRemove(allDataStoreKeys); this._collectionInMemoryIndex.clear(); } diff --git a/packages/datastore/src/storage/adapter/InMemoryStore.ts b/packages/datastore/src/storage/adapter/InMemoryStore.ts index 9a771ba0186..a51deda7f08 100644 --- a/packages/datastore/src/storage/adapter/InMemoryStore.ts +++ b/packages/datastore/src/storage/adapter/InMemoryStore.ts @@ -6,11 +6,14 @@ export class InMemoryStore { }; multiGet = async (keys: string[]) => { - return keys.reduce((res, k) => (res.push([k, this.db.get(k)]), res), []); + return keys.reduce( + (res, k) => (res.push([k, this.db.get(k)!]), res), + [] as [string, string][] + ); }; multiRemove = async (keys: string[], callback?) => { - keys.forEach(k => this.db.delete(k)); + keys.forEach((k) => this.db.delete(k)); callback(); }; diff --git a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts index 9a57bd4c5b0..00399752f1d 100644 --- a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts +++ b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts @@ -19,7 +19,6 @@ import { RelationType, } from '../../types'; import { - exhaustiveCheck, getIndex, getIndexFromAssociation, isModelConstructor, @@ -27,6 +26,7 @@ import { traverseModel, validatePredicate, inMemoryPagination, + NAMESPACES, } from '../../util'; import { Adapter } from './index'; @@ -35,17 +35,17 @@ const logger = new Logger('DataStore'); const DB_NAME = 'amplify-datastore'; class IndexedDBAdapter implements Adapter { - private schema: InternalSchema; - private namespaceResolver: NamespaceResolver; - private modelInstanceCreator: ModelInstanceCreator; - private getModelConstructorByModelName: ( - namsespaceName: string, + private schema!: InternalSchema; + private namespaceResolver!: NamespaceResolver; + private modelInstanceCreator!: ModelInstanceCreator; + private getModelConstructorByModelName?: ( + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor; - private db: idb.IDBPDatabase; - private initPromise: Promise; - private resolve: (value?: any) => void; - private reject: (value?: any) => void; + private db!: idb.IDBPDatabase; + private initPromise!: Promise; + private resolve!: (value?: any) => void; + private reject!: (value?: any) => void; private dbName: string = DB_NAME; private async checkPrivate() { @@ -82,7 +82,7 @@ class IndexedDBAdapter implements Adapter { namespaceResolver: NamespaceResolver, modelInstanceCreator: ModelInstanceCreator, getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor, sessionId?: string @@ -106,6 +106,9 @@ class IndexedDBAdapter implements Adapter { try { if (!this.db) { + // Should we consider mapping a `DBSchema` type to give to openDB() so we can + // limit the number of type casts and/or any's, and/or guards later? + // See https://github.com/jakearchibald/idb#typescript const VERSION = 2; this.db = await idb.openDB(this.dbName, VERSION, { upgrade: async (db, oldVersion, newVersion, txn) => { @@ -120,9 +123,9 @@ class IndexedDBAdapter implements Adapter { }); const indexes = - this.schema.namespaces[namespaceName].relationships[ + this.schema?.namespaces?.[namespaceName]?.relationships?.[ modelName - ].indexes; + ].indexes || []; indexes.forEach(index => store.createIndex(index, index)); store.createIndex('byId', 'id', { unique: true }); @@ -230,13 +233,17 @@ class IndexedDBAdapter implements Adapter { ); const store = tx.objectStore(storeName); - const fromDB = await this._get(store, model.id); + const fromDB = (await this._get(store, model.id)) as T | undefined; if (condition && fromDB) { const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; + const { predicates: predicateObjs, type } = predicates || {}; - const isValid = validatePredicate(fromDB, type, predicateObjs); + const isValid = validatePredicate( + fromDB, + type as any, + predicateObjs as any + ); if (!isValid) { const msg = 'Conditional update failed'; @@ -270,16 +277,16 @@ class IndexedDBAdapter implements Adapter { } private async load( - namespaceName: string, + namespaceName: NAMESPACES, srcModelName: string, records: T[] ): Promise { const namespace = this.schema.namespaces[namespaceName]; - const relations = namespace.relationships[srcModelName].relationTypes; + const relations = namespace.relationships![srcModelName].relationTypes; const connectionStoreNames = relations.map(({ modelName }) => { return this.getStorename(namespaceName, modelName); }); - const modelConstructor = this.getModelConstructorByModelName( + const modelConstructor = this.getModelConstructorByModelName!( namespaceName, srcModelName ); @@ -302,7 +309,9 @@ class IndexedDBAdapter implements Adapter { ): Promise { await this.checkPrivate(); const storeName = this.getStorenameForModel(modelConstructor); - const namespaceName = this.namespaceResolver(modelConstructor); + const namespaceName = this.namespaceResolver( + modelConstructor + ) as NAMESPACES; const predicates = predicate && ModelPredicateCreator.getPredicates(predicate); @@ -424,7 +433,7 @@ class IndexedDBAdapter implements Adapter { if (actualPredicateIndexes.length > 0) { const predicateIndex = actualPredicateIndexes[0]; candidateResults = ( - await predicateIndex.index.getAll(predicateIndex.predicate.operand) + await predicateIndex.index!.getAll(predicateIndex.predicate.operand) ); } else { // no usable indexes @@ -442,7 +451,7 @@ class IndexedDBAdapter implements Adapter { const distinctResults = new Map(); for (const predicateIndex of predicateIndexes) { const resultGroup = ( - await predicateIndex.index.getAll(predicateIndex.predicate.operand) + await predicateIndex.index!.getAll(predicateIndex.predicate.operand) ); for (const item of resultGroup) { // TODO: custom PK @@ -543,14 +552,15 @@ class IndexedDBAdapter implements Adapter { const deleteQueue: { storeName: string; items: T[] }[] = []; if (isModelConstructor(modelOrModelConstructor)) { - const modelConstructor = modelOrModelConstructor; - const nameSpace = this.namespaceResolver(modelConstructor); + const modelConstructor = + modelOrModelConstructor as PersistentModelConstructor; + const nameSpace = this.namespaceResolver(modelConstructor) as NAMESPACES; const storeName = this.getStorenameForModel(modelConstructor); - const models = await this.query(modelConstructor, condition); + const models = await this.query(modelConstructor, condition!); const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] + this.schema.namespaces![nameSpace].relationships![modelConstructor.name] .relationTypes; if (condition !== undefined) { @@ -593,11 +603,11 @@ class IndexedDBAdapter implements Adapter { return [models, deletedModels]; } } else { - const model = modelOrModelConstructor; + const model = modelOrModelConstructor as T; const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor; - const nameSpace = this.namespaceResolver(modelConstructor); + const nameSpace = this.namespaceResolver(modelConstructor) as NAMESPACES; const storeName = this.getStorenameForModel(modelConstructor); @@ -615,9 +625,10 @@ class IndexedDBAdapter implements Adapter { } const predicates = ModelPredicateCreator.getPredicates(condition); - const { predicates: predicateObjs, type } = predicates; + const { predicates: predicateObjs, type } = + predicates as PredicatesGroup; - const isValid = validatePredicate(fromDB, type, predicateObjs); + const isValid = validatePredicate(fromDB as T, type, predicateObjs); if (!isValid) { const msg = 'Conditional update failed'; @@ -628,8 +639,9 @@ class IndexedDBAdapter implements Adapter { await tx.done; const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] - .relationTypes; + this.schema.namespaces[nameSpace].relationships![ + modelConstructor.name + ].relationTypes; await this.deleteTraverse( relations, @@ -640,8 +652,9 @@ class IndexedDBAdapter implements Adapter { ); } else { const relations = - this.schema.namespaces[nameSpace].relationships[modelConstructor.name] - .relationTypes; + this.schema.namespaces[nameSpace].relationships![ + modelConstructor.name + ].relationTypes; await this.deleteTraverse( relations, @@ -666,12 +679,12 @@ class IndexedDBAdapter implements Adapter { private async deleteItem( deleteQueue?: { storeName: string; items: T[] | IDBValidKey[] }[] ) { - const connectionStoreNames = deleteQueue.map(({ storeName }) => { + const connectionStoreNames = deleteQueue!.map(({ storeName }) => { return storeName; }); const tx = this.db.transaction([...connectionStoreNames], 'readwrite'); - for await (const deleteItem of deleteQueue) { + for await (const deleteItem of deleteQueue!) { const { storeName, items } = deleteItem; const store = tx.objectStore(storeName); @@ -680,9 +693,9 @@ class IndexedDBAdapter implements Adapter { let key: IDBValidKey; if (typeof item === 'object') { - key = await store.index('byId').getKey(item['id']); + key = (await store.index('byId').getKey(item['id']))!; } else { - key = await store.index('byId').getKey(item.toString()); + key = (await store.index('byId').getKey(item.toString()))!; } if (key !== undefined) { @@ -697,7 +710,7 @@ class IndexedDBAdapter implements Adapter { relations: RelationType[], models: T[], srcModel: string, - nameSpace: string, + nameSpace: NAMESPACES, deleteQueue: { storeName: string; items: T[] }[] ): Promise { for await (const rel of relations) { @@ -706,7 +719,7 @@ class IndexedDBAdapter implements Adapter { const index: string = getIndex( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, srcModel ) || @@ -714,8 +727,8 @@ class IndexedDBAdapter implements Adapter { // i.e. for keyName connections, attempt to find one by the // associatedWith property getIndexFromAssociation( - this.schema.namespaces[nameSpace].relationships[modelName].indexes, - rel.associatedWith + this.schema.namespaces[nameSpace].relationships![modelName].indexes, + rel.associatedWith! ); switch (relationType) { @@ -723,9 +736,8 @@ class IndexedDBAdapter implements Adapter { for await (const model of models) { const hasOneIndex = index || 'byId'; - const hasOneCustomField = targetName in model; - const value = hasOneCustomField ? model[targetName] : model.id; - if (!value) break; + const hasOneCustomField = targetName! in model; + const value = hasOneCustomField ? model[targetName!] : model.id; const recordToDelete = ( await this.db @@ -736,7 +748,7 @@ class IndexedDBAdapter implements Adapter { ); await this.deleteTraverse( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, recordToDelete ? [recordToDelete] : [], modelName, @@ -754,7 +766,7 @@ class IndexedDBAdapter implements Adapter { .getAll(model['id']); await this.deleteTraverse( - this.schema.namespaces[nameSpace].relationships[modelName] + this.schema.namespaces[nameSpace].relationships![modelName] .relationTypes, childrenArray, modelName, @@ -767,7 +779,7 @@ class IndexedDBAdapter implements Adapter { // Intentionally blank break; default: - exhaustiveCheck(relationType); + throw new Error(`Invalid relation type ${relationType}`); break; } } @@ -776,7 +788,7 @@ class IndexedDBAdapter implements Adapter { storeName: this.getStorename(nameSpace, srcModel), items: models.map(record => this.modelInstanceCreator( - this.getModelConstructorByModelName(nameSpace, srcModel), + this.getModelConstructorByModelName!(nameSpace, srcModel), record ) ), @@ -790,8 +802,8 @@ class IndexedDBAdapter implements Adapter { await idb.deleteDB(this.dbName); - this.db = undefined; - this.initPromise = undefined; + this.db = undefined!; + this.initPromise = undefined!; } async batchSave( @@ -825,7 +837,7 @@ class IndexedDBAdapter implements Adapter { if (!_deleted) { const { instance } = connectedModels.find( ({ instance }) => instance.id === id - ); + )!; result.push([ (instance), diff --git a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts index 47006acfaac..53df358f7bb 100644 --- a/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts +++ b/packages/datastore/src/storage/adapter/getDefaultAdapter/index.ts @@ -7,10 +7,10 @@ const getDefaultAdapter: () => Adapter = () => { const { isBrowser } = browserOrNode(); if ((isBrowser && window.indexedDB) || (isWebWorker() && self.indexedDB)) { - return IndexedDBAdapter; + return IndexedDBAdapter as Adapter; } - return AsyncStorageAdapter; + return AsyncStorageAdapter as Adapter; }; export default getDefaultAdapter; diff --git a/packages/datastore/src/storage/storage.ts b/packages/datastore/src/storage/storage.ts index fa33c9112db..64c8cba3e98 100644 --- a/packages/datastore/src/storage/storage.ts +++ b/packages/datastore/src/storage/storage.ts @@ -24,6 +24,7 @@ import { STORAGE, validatePredicate, valuesEqual, + NAMESPACES, } from '../util'; import { Adapter } from './adapter'; import getDefaultAdapter from './adapter/getDefaultAdapter'; @@ -38,7 +39,7 @@ export type Storage = InstanceType; const logger = new Logger('DataStore'); class StorageClass implements StorageFacade { - private initialized: Promise; + private initialized: Promise | undefined; private readonly pushStream: { observable: Observable>; } & Required< @@ -49,7 +50,7 @@ class StorageClass implements StorageFacade { private readonly schema: InternalSchema, private readonly namespaceResolver: NamespaceResolver, private readonly getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor, private readonly modelInstanceCreator: ModelInstanceCreator, @@ -87,15 +88,13 @@ class StorageClass implements StorageFacade { reject = rej; }); - this.adapter - .setUp( - this.schema, - this.namespaceResolver, - this.modelInstanceCreator, - this.getModelConstructorByModelName, - this.sessionId - ) - .then(resolve, reject); + this.adapter!.setUp( + this.schema, + this.namespaceResolver, + this.modelInstanceCreator, + this.getModelConstructorByModelName, + this.sessionId + ).then(resolve!, reject!); await this.initialized; } @@ -107,6 +106,9 @@ class StorageClass implements StorageFacade { patchesTuple?: [Patch[], PersistentModel] ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { await this.init(); + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } const result = await this.adapter.save(model, condition); @@ -139,11 +141,14 @@ class StorageClass implements StorageFacade { ).constructor as PersistentModelConstructor; this.pushStream.next({ - model: modelConstructor, + model: modelConstructor as any, opType, element, mutator, - condition: ModelPredicateCreator.getPredicates(condition, false), + condition: + (condition && + ModelPredicateCreator.getPredicates(condition, false)) || + null, }); }); @@ -166,9 +171,12 @@ class StorageClass implements StorageFacade { mutator?: Symbol ): Promise<[T[], T[]]> { await this.init(); + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } - let deleted: T[]; let models: T[]; + let deleted: T[] | undefined; [models, deleted] = await this.adapter.delete( modelOrModelConstructor, @@ -188,20 +196,21 @@ class StorageClass implements StorageFacade { const modelConstructor = (Object.getPrototypeOf(model) as Object) .constructor as PersistentModelConstructor; - let theCondition: PredicatesGroup; + let theCondition: PredicatesGroup | undefined; if (!isModelConstructor(modelOrModelConstructor)) { - theCondition = modelIds.has(model.id) - ? ModelPredicateCreator.getPredicates(condition, false) - : undefined; + theCondition = + modelIds.has(model.id) && condition + ? ModelPredicateCreator.getPredicates(condition, false) + : undefined; } this.pushStream.next({ - model: modelConstructor, + model: modelConstructor as any, opType: OpType.DELETE, element: model, mutator, - condition: theCondition, + condition: theCondition || null, }); }); @@ -214,6 +223,9 @@ class StorageClass implements StorageFacade { pagination?: PaginationInput ): Promise { await this.init(); + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } return await this.adapter.query(modelConstructor, predicate, pagination); } @@ -221,22 +233,25 @@ class StorageClass implements StorageFacade { async queryOne( modelConstructor: PersistentModelConstructor, firstOrLast: QueryOne = QueryOne.FIRST - ): Promise { + ): Promise { await this.init(); + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } const record = await this.adapter.queryOne(modelConstructor, firstOrLast); return record; } observe( - modelConstructor?: PersistentModelConstructor, - predicate?: ModelPredicate, + modelConstructor?: PersistentModelConstructor | null, + predicate?: ModelPredicate | null, skipOwn?: Symbol ): Observable> { const listenToAll = !modelConstructor; const { predicates, type } = - ModelPredicateCreator.getPredicates(predicate, false) || {}; - const hasPredicate = !!predicates; + (predicate && ModelPredicateCreator.getPredicates(predicate, false)) || + {}; let result = this.pushStream.observable .filter(({ mutator }) => { @@ -252,7 +267,7 @@ class StorageClass implements StorageFacade { return false; } - if (hasPredicate) { + if (!!predicates && !!type) { return validatePredicate(element, type, predicates); } @@ -265,6 +280,9 @@ class StorageClass implements StorageFacade { async clear(completeObservable = true) { this.initialized = undefined; + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } await this.adapter.clear(); @@ -279,6 +297,9 @@ class StorageClass implements StorageFacade { mutator?: Symbol ): Promise<[T, OpType][]> { await this.init(); + if (!this.adapter) { + throw new Error('Storage adapter is missing'); + } const result = await this.adapter.batchSave(modelConstructor, items); @@ -288,7 +309,7 @@ class StorageClass implements StorageFacade { opType, element, mutator, - condition: undefined, + condition: null, }); }); @@ -320,7 +341,7 @@ class StorageClass implements StorageFacade { const { fields } = this.schema.namespaces[namespace].models[modelConstructor.name]; const { primaryKey, compositeKeys = [] } = - this.schema.namespaces[namespace].keys[modelConstructor.name]; + this.schema.namespaces[namespace].keys?.[modelConstructor.name] || {}; // set original values for these fields updatedFields.forEach((field: string) => { @@ -380,7 +401,7 @@ class ExclusiveStorage implements StorageFacade { schema: InternalSchema, namespaceResolver: NamespaceResolver, getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor, modelInstanceCreator: ModelInstanceCreator, @@ -431,11 +452,11 @@ class ExclusiveStorage implements StorageFacade { if (isModelConstructor(modelOrModelConstructor)) { const modelConstructor = modelOrModelConstructor; - return storage.delete(modelConstructor, condition, mutator); + return storage.delete(modelConstructor as any, condition, mutator); } else { const model = modelOrModelConstructor; - return storage.delete(model, condition, mutator); + return storage.delete(model as any, condition, mutator); } }); } @@ -453,8 +474,8 @@ class ExclusiveStorage implements StorageFacade { async queryOne( modelConstructor: PersistentModelConstructor, firstOrLast: QueryOne = QueryOne.FIRST - ): Promise { - return this.runExclusive(storage => + ): Promise { + return this.runExclusive(storage => storage.queryOne(modelConstructor, firstOrLast) ); } @@ -464,8 +485,8 @@ class ExclusiveStorage implements StorageFacade { } observe( - modelConstructor?: PersistentModelConstructor, - predicate?: ModelPredicate, + modelConstructor?: PersistentModelConstructor | null, + predicate?: ModelPredicate | null, 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 78f7d08f277..989c29b2c0e 100644 --- a/packages/datastore/src/sync/datastoreConnectivity.ts +++ b/packages/datastore/src/sync/datastoreConnectivity.ts @@ -13,9 +13,9 @@ type ConnectionStatus = { export default class DataStoreConnectivity { private connectionStatus: ConnectionStatus; - private observer: ZenObservable.SubscriptionObserver; - private subscription: ZenObservable.Subscription; - private timeout: ReturnType; + private observer!: ZenObservable.SubscriptionObserver; + private subscription!: ZenObservable.Subscription; + private timeout!: ReturnType; constructor() { this.connectionStatus = { online: false, @@ -26,7 +26,7 @@ export default class DataStoreConnectivity { if (this.observer) { throw new Error('Subscriber already exists'); } - return new Observable(observer => { + return new Observable((observer) => { this.observer = observer; // Will be used to forward socket connection changes, enhancing Reachability diff --git a/packages/datastore/src/sync/index.ts b/packages/datastore/src/sync/index.ts index 22c4eee6840..3f43aa38395 100644 --- a/packages/datastore/src/sync/index.ts +++ b/packages/datastore/src/sync/index.ts @@ -22,7 +22,7 @@ import { ModelPredicate, AuthModeStrategy, } from '../types'; -import { exhaustiveCheck, getNow, SYNC, USER } from '../util'; +import { getNow, SYNC, USER } from '../util'; import DataStoreConnectivity from './datastoreConnectivity'; import { ModelMerger } from './merger'; import { MutationEventOutbox } from './outbox'; @@ -68,9 +68,9 @@ declare class ModelMetadata { public readonly namespace: string; public readonly model: string; public readonly fullSyncInterval: number; - public readonly lastSync?: number; - public readonly lastFullSync?: number; - public readonly lastSyncPredicate?: null | string; + public readonly lastSync?: number | null; + public readonly lastFullSync?: number | null; + public readonly lastSyncPredicate?: string | null; } export enum ControlMessage { @@ -103,7 +103,7 @@ export class SyncEngine { public getModelSyncedStatus( modelConstructor: PersistentModelConstructor ): boolean { - return this.modelSyncedStatus.get(modelConstructor); + return this.modelSyncedStatus.get(modelConstructor)!; } constructor( @@ -121,7 +121,7 @@ export class SyncEngine { ) { const MutationEvent = this.modelClasses[ 'MutationEvent' - ] as PersistentModelConstructor; + ] as PersistentModelConstructor; this.outbox = new MutationEventOutbox( this.schema, @@ -311,7 +311,7 @@ export class SyncEngine { // TODO: extract to function if (!isNode) { subscriptions.push( - dataSubsObservable.subscribe( + dataSubsObservable!.subscribe( ([_transformerMutationType, modelDefinition, item]) => { const modelConstructor = this.userModelClasses[ modelDefinition.name @@ -362,9 +362,9 @@ export class SyncEngine { const MutationEventConstructor = this.modelClasses[ 'MutationEvent' ] as PersistentModelConstructor; - const graphQLCondition = predicateToGraphQLCondition(condition); + const graphQLCondition = predicateToGraphQLCondition(condition!); const mutationEvent = createMutationInstanceFromModelOperation( - namespace.relationships, + namespace.relationships!, this.getModelDefinition(model), opType, model, @@ -438,7 +438,7 @@ export class SyncEngine { fullSyncInterval, lastSyncPredicate, }) => { - const nextFullSync = lastFullSync + fullSyncInterval; + const nextFullSync = lastFullSync! + fullSyncInterval; const syncFrom = !lastFullSync || nextFullSync < currentTimeStamp ? 0 // perform full sync if expired @@ -446,7 +446,7 @@ export class SyncEngine { return [ this.schema.namespaces[namespace].models[model], - [namespace, syncFrom], + [namespace, syncFrom!], ]; } ) @@ -556,7 +556,7 @@ export class SyncEngine { )) ); - const counts = count.get(modelConstructor); + const counts = count.get(modelConstructor)!; opTypeCount.forEach(([, opType]) => { switch (opType) { @@ -570,7 +570,7 @@ export class SyncEngine { counts.deleted++; break; default: - exhaustiveCheck(opType); + throw new Error(`Invalid opType ${opType}`); } }); }); @@ -590,18 +590,18 @@ export class SyncEngine { newestFullSyncStartedAt = newestFullSyncStartedAt === undefined - ? lastFullSync + ? lastFullSync! : Math.max( newestFullSyncStartedAt, - isFullSync ? startedAt : lastFullSync + isFullSync ? startedAt : lastFullSync! ); modelMetadata = ( this.modelClasses .ModelMetadata as PersistentModelConstructor ).copyOf(modelMetadata, draft => { - draft.lastSync = startedAt; - draft.lastFullSync = isFullSync + (draft.lastSync as any) = startedAt; + (draft.lastFullSync as any) = isFullSync ? startedAt : modelMetadata.lastFullSync; }); @@ -653,9 +653,9 @@ export class SyncEngine { }); const msNextFullSync = - newestFullSyncStartedAt + - theInterval - - (newestStartedAt + duration); + newestFullSyncStartedAt! + + theInterval! - + (newestStartedAt! + duration!); logger.debug( `Next fullSync in ${msNextFullSync / 1000} seconds. (${new Date( @@ -722,7 +722,7 @@ export class SyncEngine { const promises = models.map(async ([namespace, model]) => { const modelMetadata = await this.getModelMetadata(namespace, model.name); const syncPredicate = ModelPredicateCreator.getPredicates( - this.syncPredicates.get(model), + this.syncPredicates.get(model)!, false ); const lastSyncPredicate = syncPredicate @@ -752,13 +752,13 @@ export class SyncEngine { ( this.modelClasses.ModelMetadata as PersistentModelConstructor ).copyOf(modelMetadata, draft => { - draft.fullSyncInterval = fullSyncInterval; + (draft.fullSyncInterval as any) = fullSyncInterval; // perform a base sync if the syncPredicate changed in between calls to DataStore.start // ensures that the local store contains all the data specified by the syncExpression if (syncPredicateUpdated) { draft.lastSync = null; draft.lastFullSync = null; - draft.lastSyncPredicate = lastSyncPredicate; + (draft.lastSyncPredicate as any) = lastSyncPredicate; } }) ); diff --git a/packages/datastore/src/sync/merger.ts b/packages/datastore/src/sync/merger.ts index c51cdd22119..ae99f5d7c33 100644 --- a/packages/datastore/src/sync/merger.ts +++ b/packages/datastore/src/sync/merger.ts @@ -29,7 +29,7 @@ class ModelMerger { } } - return result; + return result!; } public async mergePage( diff --git a/packages/datastore/src/sync/outbox.ts b/packages/datastore/src/sync/outbox.ts index f5167595591..920351d5f5c 100644 --- a/packages/datastore/src/sync/outbox.ts +++ b/packages/datastore/src/sync/outbox.ts @@ -17,7 +17,7 @@ import { TransformerMutationType } from './utils'; // TODO: Persist deleted ids class MutationEventOutbox { - private inProgressMutationEventId: string; + private inProgressMutationEventId!: string; constructor( private readonly schema: InternalSchema, @@ -81,7 +81,7 @@ class MutationEventOutbox { await s.delete(this.MutationEvent, predicate); } - merged = merged || mutationEvent; + merged = merged! || mutationEvent; // Enqueue new one await s.save(merged, undefined, this.ownSymbol); @@ -97,11 +97,11 @@ class MutationEventOutbox { const head = await this.peek(storage); if (record) { - await this.syncOutboxVersionsOnDequeue(storage, record, head, recordOp); + await this.syncOutboxVersionsOnDequeue(storage, record, head, recordOp!); } await storage.delete(head); - this.inProgressMutationEventId = undefined; + this.inProgressMutationEventId = undefined!; return head; } @@ -114,9 +114,9 @@ class MutationEventOutbox { public async peek(storage: StorageFacade): Promise { const head = await storage.queryOne(this.MutationEvent, QueryOne.FIRST); - this.inProgressMutationEventId = head ? head.id : undefined; + this.inProgressMutationEventId = head ? head.id : undefined!; - return head; + return head!; } public async getForModel( diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index ccff049a746..97846e6a8ec 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -24,7 +24,7 @@ import { SchemaModel, TypeConstructorMap, } from '../../types'; -import { exhaustiveCheck, USER } from '../../util'; +import { USER } from '../../util'; import { MutationEventOutbox } from '../outbox'; import { buildGraphQLOperation, @@ -46,7 +46,7 @@ type MutationProcessorEvent = { }; class MutationProcessor { - private observer: ZenObservable.Observer; + private observer!: ZenObservable.Observer; private readonly typeQuery = new WeakMap< SchemaModel, [TransformerMutationType, string, string][] @@ -69,10 +69,10 @@ class MutationProcessor { } private generateQueries() { - Object.values(this.schema.namespaces).forEach(namespace => { + Object.values(this.schema.namespaces).forEach((namespace) => { Object.values(namespace.models) .filter(({ syncable }) => syncable) - .forEach(model => { + .forEach((model) => { const [createMutation] = buildGraphQLOperation( namespace, model, @@ -103,7 +103,7 @@ class MutationProcessor { } public start(): Observable { - const observable = new Observable(observer => { + const observable = new Observable((observer) => { this.observer = observer; this.resume(); @@ -136,7 +136,7 @@ class MutationProcessor { ] as PersistentModelConstructor; let result: GraphQLResult>; let opName: string; - let modelDefinition: SchemaModel; + let modelDefinition!: SchemaModel; try { const modelAuthModes = await getModelAuthModes({ authModeStrategy: this.authModeStrategy, @@ -159,7 +159,7 @@ class MutationProcessor { operation, data, condition, - modelConstructor, + modelConstructor as any, this.MutationEvent, head, operationAuthModes[authModeAttempts] @@ -198,25 +198,25 @@ class MutationProcessor { } } - if (result === undefined) { + if (result! === undefined) { logger.debug('done retrying'); - await this.storage.runExclusive(async storage => { + await this.storage.runExclusive(async (storage) => { await this.outbox.dequeue(storage); }); continue; } - const record = result.data[opName]; + const record = result!.data![opName!]; let hasMore = false; - await this.storage.runExclusive(async storage => { + await this.storage.runExclusive(async (storage) => { // using runExclusive to prevent possible race condition // when another record gets enqueued between dequeue and peek await this.outbox.dequeue(storage, record, operation); hasMore = (await this.outbox.peek(storage)) !== undefined; }); - this.observer.next({ + this.observer.next!({ operation, modelDefinition, model: record, @@ -251,19 +251,14 @@ class MutationProcessor { MutationEvent: PersistentModelConstructor, mutationEvent: MutationEvent ) => { - const [ - query, - variables, - graphQLCondition, - opName, - modelDefinition, - ] = this.createQueryVariables( - namespaceName, - model, - operation, - data, - condition - ); + const [query, variables, graphQLCondition, opName, modelDefinition] = + this.createQueryVariables( + namespaceName, + model, + operation, + data, + condition + ); const authToken = await getTokenForCustomAuth( authMode, @@ -310,7 +305,7 @@ class MutationProcessor { retryWith = DISCARD; } else { try { - retryWith = await this.conflictHandler({ + retryWith = await this.conflictHandler!({ modelConstructor, localModel: this.modelInstanceCreator( modelConstructor, @@ -358,24 +353,25 @@ class MutationProcessor { const namespace = this.schema.namespaces[namespaceName]; // convert retry with to tryWith - const updatedMutation = createMutationInstanceFromModelOperation( - namespace.relationships, - modelDefinition, - opType, - modelConstructor, - retryWith, - graphQLCondition, - MutationEvent, - this.modelInstanceCreator, - mutationEvent.id - ); + const updatedMutation = + createMutationInstanceFromModelOperation( + namespace.relationships!, + modelDefinition, + opType, + modelConstructor, + retryWith, + graphQLCondition, + MutationEvent, + this.modelInstanceCreator, + mutationEvent.id + ); await this.storage.save(updatedMutation); throw new NonRetryableError('RetryMutation'); } else { try { - await this.errorHandler({ + await this.errorHandler!({ localModel: this.modelInstanceCreator( modelConstructor, variables.input @@ -386,7 +382,7 @@ class MutationProcessor { errorInfo: error.errorInfo, remoteModel: error.data ? this.modelInstanceCreator(modelConstructor, error.data) - : null, + : null!, }); } catch (err) { logger.warn('failed to execute errorHandler', err); @@ -429,11 +425,11 @@ class MutationProcessor { condition: string ): [string, Record, GraphQLCondition, string, SchemaModel] { const modelDefinition = this.schema.namespaces[namespaceName].models[model]; - const { primaryKey } = this.schema.namespaces[namespaceName].keys[model]; + const { primaryKey } = this.schema.namespaces[namespaceName].keys![model]; const queriesTuples = this.typeQuery.get(modelDefinition); - const [, opName, query] = queriesTuples.find( + const [, opName, query] = queriesTuples!.find( ([transformerMutationType]) => transformerMutationType === operation ); @@ -530,8 +526,11 @@ class MutationProcessor { case TransformerMutationType.GET: // Intentionally blank break; default: - exhaustiveCheck(operation); + throw new Error(`Invalid operation ${operation}`); } + + // make TS happy ... + return undefined!; } public pause() { diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index 39c1345cd8b..b62f0cc6f0c 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -48,12 +48,9 @@ class SubscriptionProcessor { SchemaModel, [TransformerMutationType, string, string][] >(); - private buffer: [ - TransformerMutationType, - SchemaModel, - PersistentModel - ][] = []; - private dataObserver: ZenObservable.Observer; + private buffer: [TransformerMutationType, SchemaModel, PersistentModel][] = + []; + private dataObserver!: ZenObservable.Observer; constructor( private readonly schema: InternalSchema, @@ -95,7 +92,7 @@ class SubscriptionProcessor { model, transformerMutationType, isOwner, - ownerField + ownerField! ); return { authMode, opType, opName, query, isOwner, ownerField, ownerValue }; } @@ -114,11 +111,11 @@ class SubscriptionProcessor { const iamPrivateAuth = authMode === GRAPHQL_AUTH_MODE.AWS_IAM && rules.find( - rule => rule.authStrategy === 'private' && rule.provider === 'iam' + (rule) => rule.authStrategy === 'private' && rule.provider === 'iam' ); if (iamPrivateAuth && userCredentials === USER_CREDENTIALS.unauth) { - return null; + return null!; } // Group auth should take precedence over owner auth, so we are checking @@ -126,7 +123,7 @@ class SubscriptionProcessor { // OIDC token has a groupClaim. If so, we are returning auth info before // any further owner-based auth checks. const groupAuthRules = rules.filter( - rule => + (rule) => rule.authStrategy === 'groups' && ['userPools', 'oidc'].includes(rule.provider) ); @@ -134,7 +131,7 @@ class SubscriptionProcessor { const validGroup = (authMode === GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS || authMode === GRAPHQL_AUTH_MODE.OPENID_CONNECT) && - groupAuthRules.find(groupAuthRule => { + groupAuthRules.find((groupAuthRule) => { // validate token against groupClaim const cognitoUserGroups = getUserGroupsFromToken( cognitoTokenPayload, @@ -145,8 +142,8 @@ class SubscriptionProcessor { groupAuthRule ); - return [...cognitoUserGroups, ...oidcUserGroups].find(userGroup => { - return groupAuthRule.groups.find(group => group === userGroup); + return [...cognitoUserGroups, ...oidcUserGroups].find((userGroup) => { + return groupAuthRule.groups.find((group) => group === userGroup); }); }); @@ -163,13 +160,13 @@ class SubscriptionProcessor { const cognitoOwnerAuthRules = authMode === GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ? rules.filter( - rule => + (rule) => rule.authStrategy === 'owner' && rule.provider === 'userPools' ) : []; let ownerAuthInfo: AuthorizationInfo; - cognitoOwnerAuthRules.forEach(ownerAuthRule => { + cognitoOwnerAuthRules.forEach((ownerAuthRule) => { const ownerValue = cognitoTokenPayload[ownerAuthRule.identityClaim]; if (ownerValue) { @@ -182,8 +179,8 @@ class SubscriptionProcessor { } }); - if (ownerAuthInfo) { - return ownerAuthInfo; + if (ownerAuthInfo!) { + return ownerAuthInfo!; } // Owner auth needs additional values to be returned in order to create the subscription with @@ -192,11 +189,11 @@ class SubscriptionProcessor { const oidcOwnerAuthRules = authMode === GRAPHQL_AUTH_MODE.OPENID_CONNECT ? rules.filter( - rule => rule.authStrategy === 'owner' && rule.provider === 'oidc' + (rule) => rule.authStrategy === 'owner' && rule.provider === 'oidc' ) : []; - oidcOwnerAuthRules.forEach(ownerAuthRule => { + oidcOwnerAuthRules.forEach((ownerAuthRule) => { const ownerValue = oidcTokenPayload[ownerAuthRule.identityClaim]; if (ownerValue) { @@ -209,8 +206,8 @@ class SubscriptionProcessor { } }); - if (ownerAuthInfo) { - return ownerAuthInfo; + if (ownerAuthInfo!) { + return ownerAuthInfo!; } // Fallback: return authMode or default auth type @@ -234,7 +231,7 @@ class SubscriptionProcessor { Observable, Observable<[TransformerMutationType, SchemaModel, PersistentModel]> ] { - const ctlObservable = new Observable(observer => { + const ctlObservable = new Observable((observer) => { const promises: Promise[] = []; // Creating subs for each model/operation combo so they can be unsubscribed @@ -302,14 +299,14 @@ class SubscriptionProcessor { // best effort to get oidc jwt } - Object.values(this.schema.namespaces).forEach(namespace => { + Object.values(this.schema.namespaces).forEach((namespace) => { Object.values(namespace.models) .filter(({ syncable }) => syncable) - .forEach(async modelDefinition => { + .forEach(async (modelDefinition) => { const modelAuthModes = await getModelAuthModes({ authModeStrategy: this.authModeStrategy, - defaultAuthMode: this.amplifyConfig - .aws_appsync_authenticationType, + defaultAuthMode: + this.amplifyConfig.aws_appsync_authenticationType, modelName: modelDefinition.name, schema: this.schema, }); @@ -339,7 +336,7 @@ class SubscriptionProcessor { }; // Retry failed subscriptions with next auth mode (if available) - const authModeRetry = async operation => { + const authModeRetry = async (operation) => { const { opType: transformerMutationType, opName, @@ -373,7 +370,7 @@ class SubscriptionProcessor { return; } - variables[ownerField] = ownerValue; + variables[ownerField!] = ownerValue; } logger.debug( @@ -413,12 +410,13 @@ class SubscriptionProcessor { return; } - const predicatesGroup = ModelPredicateCreator.getPredicates( - this.syncPredicates.get(modelDefinition), - false - ); + const predicatesGroup = + ModelPredicateCreator.getPredicates( + this.syncPredicates.get(modelDefinition)!, + false + ); - const { [opName]: record } = data; + const { [opName]: record } = data!; // checking incoming subscription against syncPredicate. // once AppSync implements filters on subscriptions, we'll be @@ -427,7 +425,7 @@ class SubscriptionProcessor { if ( this.passesPredicateValidation( record, - predicatesGroup + predicatesGroup! ) ) { this.pushToBuffer( @@ -438,7 +436,7 @@ class SubscriptionProcessor { } this.drainBuffer(); }, - error: subscriptionError => { + error: (subscriptionError) => { const { error: { errors: [{ message = '' } = {}] } = { errors: [], @@ -454,7 +452,9 @@ class SubscriptionProcessor { // Unsubscribe and clear subscription array for model/operation subscriptions[modelDefinition.name][ transformerMutationType - ].forEach(subscription => subscription.unsubscribe()); + ].forEach((subscription) => + subscription.unsubscribe() + ); subscriptions[modelDefinition.name][ transformerMutationType ] = []; @@ -512,7 +512,7 @@ class SubscriptionProcessor { (async () => { let boundFunction: any; - await new Promise(res => { + await new Promise((res) => { subscriptionReadyCallback = res; boundFunction = this.hubQueryCompletionListener.bind( this, @@ -525,7 +525,7 @@ class SubscriptionProcessor { ); }; - operations.forEach(op => authModeRetry(op)); + operations.forEach((op) => authModeRetry(op)); }); }); @@ -533,28 +533,28 @@ class SubscriptionProcessor { })(); return () => { - Object.keys(subscriptions).forEach(modelName => { - subscriptions[modelName][ - TransformerMutationType.CREATE - ].forEach(subscription => subscription.unsubscribe()); - subscriptions[modelName][ - TransformerMutationType.UPDATE - ].forEach(subscription => subscription.unsubscribe()); - subscriptions[modelName][ - TransformerMutationType.DELETE - ].forEach(subscription => subscription.unsubscribe()); + Object.keys(subscriptions).forEach((modelName) => { + subscriptions[modelName][TransformerMutationType.CREATE].forEach( + (subscription) => subscription.unsubscribe() + ); + subscriptions[modelName][TransformerMutationType.UPDATE].forEach( + (subscription) => subscription.unsubscribe() + ); + subscriptions[modelName][TransformerMutationType.DELETE].forEach( + (subscription) => subscription.unsubscribe() + ); }); }; }); const dataObservable = new Observable< [TransformerMutationType, SchemaModel, PersistentModel] - >(observer => { + >((observer) => { this.dataObserver = observer; this.drainBuffer(); return () => { - this.dataObserver = null; + this.dataObserver = null!; }; }); @@ -584,7 +584,7 @@ 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 = []; } } diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index d8ecdecbfde..dea46242f4e 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -46,10 +46,10 @@ class SyncProcessor { } private generateQueries() { - Object.values(this.schema.namespaces).forEach(namespace => { + Object.values(this.schema.namespaces).forEach((namespace) => { Object.values(namespace.models) .filter(({ syncable }) => syncable) - .forEach(model => { + .forEach((model) => { const [[, ...opNameQuery]] = buildGraphQLOperation( namespace, model, @@ -63,15 +63,16 @@ class SyncProcessor { private graphqlFilterFromPredicate(model: SchemaModel): GraphQLFilter { if (!this.syncPredicates) { - return null; + return null!; } - const predicatesGroup: PredicatesGroup = ModelPredicateCreator.getPredicates( - this.syncPredicates.get(model), - false - ); + const predicatesGroup: PredicatesGroup = + ModelPredicateCreator.getPredicates( + this.syncPredicates.get(model)!, + false + )!; if (!predicatesGroup) { - return null; + return null!; } return predicateToGraphQLFilter(predicatesGroup); @@ -83,7 +84,7 @@ class SyncProcessor { modelDefinition: SchemaModel, lastSync: number, nextToken: string, - limit: number = null, + limit: number = null!, filter: GraphQLFilter ): Promise<{ nextToken: string; startedAt: number; items: T[] }> { const [opName, query] = this.typeQuery.get(modelDefinition); @@ -227,7 +228,7 @@ class SyncProcessor { if (hasItems) { const result = error; result.data[opName].items = result.data[opName].items.filter( - item => item !== null + (item) => item !== null ); if (error.errors) { @@ -251,14 +252,14 @@ class SyncProcessor { error && error.errors && (error.errors as [any]).some( - err => err.errorType === 'Unauthorized' + (err) => err.errorType === 'Unauthorized' ); if (unauthorized) { const result = error; if (hasItems) { result.data[opName].items = result.data[opName].items.filter( - item => item !== null + (item) => item !== null ); } else { result.data[opName] = { @@ -286,14 +287,14 @@ class SyncProcessor { let processing = true; const { maxRecordsToSync, syncPageSize } = this.amplifyConfig; const parentPromises = new Map>(); - const observable = new Observable(observer => { + const observable = new Observable((observer) => { const sortedTypesLastSyncs = Object.values(this.schema.namespaces).reduce( (map, namespace) => { for (const modelName of Array.from( - namespace.modelTopologicalOrdering.keys() + namespace.modelTopologicalOrdering!.keys() )) { const typeLastSync = typesLastSync.get(namespace.models[modelName]); - map.set(namespace.models[modelName], typeLastSync); + map.set(namespace.models[modelName], typeLastSync!); } return map; }, @@ -304,21 +305,21 @@ class SyncProcessor { .filter(([{ syncable }]) => syncable) .map(async ([modelDefinition, [namespace, lastSync]]) => { let done = false; - let nextToken: string = null; - let startedAt: number = null; - let items: ModelInstanceMetadata[] = null; + let nextToken: string = null!; + let startedAt: number = null!; + let items: ModelInstanceMetadata[] = null!; let recordsReceived = 0; const filter = this.graphqlFilterFromPredicate(modelDefinition); const parents = this.schema.namespaces[ namespace - ].modelTopologicalOrdering.get(modelDefinition.name); - const promises = parents.map(parent => + ].modelTopologicalOrdering!.get(modelDefinition.name); + const promises = parents!.map((parent) => parentPromises.get(`${namespace}_${parent}`) ); - const promise = new Promise(async res => { + const promise = new Promise(async (res) => { await Promise.all(promises); do { diff --git a/packages/datastore/src/sync/utils.ts b/packages/datastore/src/sync/utils.ts index ad06de6cdfb..c82a9399cfd 100644 --- a/packages/datastore/src/sync/utils.ts +++ b/packages/datastore/src/sync/utils.ts @@ -26,8 +26,8 @@ import { ModelOperation, InternalSchema, AuthModeStrategy, + ModelAttributes, } from '../types'; -import { exhaustiveCheck } from '../util'; import { MutationEvent } from './'; const logger = new Logger('DataStore'); @@ -47,7 +47,7 @@ export enum TransformerMutationType { GET = 'Get', } -const dummyMetadata: Omit = { +const dummyMetadata: Partial> = { _version: undefined, _lastChangedAt: undefined, _deleted: undefined, @@ -104,9 +104,11 @@ function getOwnerFields( ): string[] { const ownerFields: string[] = []; if (isSchemaModel(modelDefinition) && modelDefinition.attributes) { - modelDefinition.attributes.forEach(attr => { + 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( + (rule) => rule.allow === 'owner' + ); if (rule && rule.ownerField) { ownerFields.push(rule.ownerField); } @@ -122,7 +124,7 @@ function getScalarFields( const { fields } = modelDefinition; const result = Object.values(fields) - .filter(field => { + .filter((field) => { if (isGraphQLScalarType(field.type) || isEnumFieldType(field.type)) { return true; } @@ -139,16 +141,17 @@ function getScalarFields( } function getConnectionFields(modelDefinition: SchemaModel): string[] { - const result = []; + const result: string[] = []; Object.values(modelDefinition.fields) .filter(({ association }) => association && Object.keys(association).length) .forEach(({ name, association }) => { - const { connectionType } = association; + const { connectionType } = association || {}; switch (connectionType) { case 'HAS_ONE': case 'HAS_MANY': + // case 'MANY_TO_MANY': // Intentionally blank break; case 'BELONGS_TO': @@ -157,7 +160,7 @@ function getConnectionFields(modelDefinition: SchemaModel): string[] { } break; default: - exhaustiveCheck(connectionType); + throw new Error(`Invalid connection type ${connectionType}`); } }); @@ -168,7 +171,7 @@ function getNonModelFields( namespace: SchemaNamespace, modelDefinition: SchemaModel | SchemaNonModel ): string[] { - const result = []; + const result: string[] = []; Object.values(modelDefinition.fields).forEach(({ name, type }) => { if (isNonModelFieldType(type)) { @@ -177,13 +180,12 @@ function getNonModelFields( ({ name }) => name ); - const nested = []; - Object.values(typeDefinition.fields).forEach(field => { + const nested: string[] = []; + Object.values(typeDefinition.fields).forEach((field) => { const { type, name } = field; if (isNonModelFieldType(type)) { const typeDefinition = namespace.nonModels![type.nonModel]; - nested.push( `${name} { ${generateSelectionSet(namespace, typeDefinition)} }` ); @@ -201,15 +203,15 @@ export function getAuthorizationRules( modelDefinition: SchemaModel ): AuthorizationRule[] { // Searching for owner authorization on attributes - const authConfig = [] - .concat(modelDefinition.attributes) - .find(attr => attr && attr.type === 'auth'); + const authConfig = ([] as ModelAttributes) + .concat(modelDefinition.attributes || []) + .find((attr) => attr && attr.type === 'auth'); const { properties: { rules = [] } = {} } = authConfig || {}; const resultRules: AuthorizationRule[] = []; // Multiple rules can be declared for allow: owner - rules.forEach(rule => { + rules.forEach((rule) => { // setting defaults for backwards compatibility with old cli const { identityClaim = 'cognito:username', @@ -241,9 +243,9 @@ export function getAuthorizationRules( if (isOwnerAuth) { // look for the subscription level override // only pay attention to the public level - const modelConfig = ([]) - .concat(modelDefinition.attributes) - .find(attr => attr && attr.type === 'model'); + const modelConfig = ([] as ModelAttributes) + .concat(modelDefinition.attributes || []) + .find((attr) => attr && attr.type === 'model'); // find the subscriptions level. ON is default const { properties: { subscriptions: { level = 'on' } = {} } = {} } = @@ -308,8 +310,8 @@ export function buildGraphQLOperation( const { name: typeName, pluralName: pluralTypeName } = modelDefinition; let operation: string; - let documentArgs: string = ' '; - let operationArgs: string = ' '; + let documentArgs: string; + let operationArgs: string; let transformerMutationType: TransformerMutationType; switch (graphQLOpType) { @@ -348,17 +350,16 @@ export function buildGraphQLOperation( operationArgs = '(id: $id)'; transformerMutationType = TransformerMutationType.GET; break; - default: - exhaustiveCheck(graphQLOpType); + throw new Error(`Invalid graphQlOpType ${graphQLOpType}`); } return [ [ - transformerMutationType, - operation, + transformerMutationType!, + operation!, `${GraphQLOperationType[graphQLOpType]} operation${documentArgs}{ - ${operation}${operationArgs}{ + ${operation!}${operationArgs}{ ${selectionSet} } }`, @@ -392,7 +393,7 @@ export function createMutationInstanceFromModelOperation< operation = TransformerMutationType.DELETE; break; default: - exhaustiveCheck(opType); + throw new Error(`Invalid opType ${opType}`); } // stringify nested objects of type AWSJSON @@ -417,7 +418,7 @@ export function createMutationInstanceFromModelOperation< data: JSON.stringify(element, replacer), modelId: element.id, model: model.name, - operation, + operation: operation!, condition: JSON.stringify(condition), }); @@ -433,7 +434,7 @@ export function predicateToGraphQLCondition( return result; } - predicate.predicates.forEach(p => { + predicate.predicates.forEach((p) => { if (isPredicateObj(p)) { const { field, operator, operand } = p; @@ -464,10 +465,10 @@ export function predicateToGraphQLFilter( result[type] = isList ? [] : {}; - const appendToFilter = value => + const appendToFilter = (value) => isList ? result[type].push(value) : (result[type] = value); - predicates.forEach(predicate => { + predicates.forEach((predicate) => { if (isPredicateObj(predicate)) { const { field, operator, operand } = predicate; @@ -515,11 +516,9 @@ export async function getModelAuthModes({ defaultAuthMode: GRAPHQL_AUTH_MODE; modelName: string; schema: InternalSchema; -}): Promise< - { - [key in ModelOperation]: GRAPHQL_AUTH_MODE[]; - } -> { +}): Promise<{ + [key in ModelOperation]: GRAPHQL_AUTH_MODE[]; +}> { const operations = Object.values(ModelOperation); const modelAuthModes: { @@ -533,7 +532,7 @@ export async function getModelAuthModes({ try { await Promise.all( - operations.map(async operation => { + operations.map(async (operation) => { const authModes = await authModeStrategy({ schema, modelName, @@ -563,7 +562,7 @@ export function getForbiddenError(error) { ]; let forbiddenError; if (error && error.errors) { - forbiddenError = (error.errors as [any]).find(err => + forbiddenError = (error.errors as [any]).find((err) => forbiddenErrorMessages.includes(err.message) ); } else if (error && error.message) { @@ -581,7 +580,7 @@ export function getClientSideAuthError(error) { const clientSideError = error && error.message && - clientSideAuthErrors.find(clientError => + clientSideAuthErrors.find((clientError) => error.message.includes(clientError) ); return clientSideError || null; diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index 47a54140e60..225f023da6e 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -1,6 +1,5 @@ import { ModelInstanceCreator } from './datastore/datastore'; import { - exhaustiveCheck, isAWSDate, isAWSTime, isAWSDateTime, @@ -10,6 +9,7 @@ import { isAWSURL, isAWSPhone, isAWSIPAddress, + NAMESPACES, } from './util'; import { PredicateAll } from './predicates'; import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api-graphql'; @@ -225,7 +225,7 @@ export namespace GraphQLScalarType { typeof GraphQLScalarType, 'getJSType' | 'getValidationFunction' > - ): 'string' | 'number' | 'boolean' | 'object' { + ) { switch (scalar) { case 'Boolean': return 'boolean'; @@ -246,7 +246,7 @@ export namespace GraphQLScalarType { case 'AWSJSON': return 'object'; default: - exhaustiveCheck(scalar as never); + throw new Error('Invalid scalar type'); } } @@ -255,7 +255,7 @@ export namespace GraphQLScalarType { typeof GraphQLScalarType, 'getJSType' | 'getValidationFunction' > - ): ((val: string | number) => boolean) | undefined { + ): ((val: string) => boolean) | ((val: number) => boolean) | undefined { switch (scalar) { case 'AWSDate': return isAWSDate; @@ -374,16 +374,65 @@ export type PersistentModelMetaData = { readOnlyFields: string; }; +export interface AsyncCollection extends AsyncIterable { + toArray(options?: { max?: number }): Promise; +} + +export type SettableFieldType = T extends Promise + ? InnerPromiseType + : T extends AsyncCollection + ? InnerCollectionType[] | undefined + : T; + +export type PredicateFieldType = NonNullable< + Scalar< + T extends Promise + ? InnerPromiseType + : T extends AsyncCollection + ? InnerCollectionType + : T + > +>; + +type KeysOfType = { + [P in keyof T]: T[P] extends FilterType ? P : never; +}[keyof T]; + +type KeysOfSuperType = { + [P in keyof T]: FilterType extends T[P] ? P : never; +}[keyof T]; + +type OptionalRelativesOf = + | KeysOfType> + | KeysOfSuperType>; + +type OmitOptionalRelatives = Omit>; +type PickOptionalRelatives = Pick>; + export type PersistentModel = Readonly<{ id: string } & Record>; + export type ModelInit< T, K extends PersistentModelMetaData = { readOnlyFields: 'createdAt' | 'updatedAt'; } -> = Omit; +> = { + [P in keyof OmitOptionalRelatives< + Omit + >]: SettableFieldType; +} & { + [P in keyof PickOptionalRelatives< + Omit + >]+?: SettableFieldType; +}; + type DeepWritable = { -readonly [P in keyof T]: T[P] extends TypeName ? T[P] + : T[P] extends Promise + ? InnerPromiseType + : T[P] extends AsyncCollection + ? InnerCollectionType[] | undefined : DeepWritable; }; @@ -392,8 +441,6 @@ export type MutableModel< K extends PersistentModelMetaData = { readOnlyFields: 'createdAt' | 'updatedAt'; } - // 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 > = DeepWritable> & Readonly>; @@ -623,10 +670,10 @@ export type SystemComponent = { namespaceResolver: NamespaceResolver, modelInstanceCreator: ModelInstanceCreator, getModelConstructorByModelName: ( - namsespaceName: string, + namsespaceName: NAMESPACES, modelName: string ) => PersistentModelConstructor, - appId: string + appId?: string ): Promise; }; diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index 82075f2e407..dc87c8d0e7c 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -30,12 +30,6 @@ import { import { WordArray } from 'amazon-cognito-identity-js'; import { ModelSortPredicateCreator } from './predicates'; -export const exhaustiveCheck = (obj: never, throwOnError: boolean = true) => { - if (throwOnError) { - throw new Error(`Invalid ${obj}`); - } -}; - export const isNullOrUndefined = (val: any): boolean => { return typeof val === 'undefined' || val === undefined || val === null; }; @@ -64,7 +58,7 @@ export const validatePredicate = ( filterType = 'some'; break; default: - exhaustiveCheck(groupType); + throw new Error(`Invalid ${groupType}`); } const result: boolean = predicatesOrGroups[filterType](predicateOrGroup => { @@ -123,7 +117,6 @@ export const validatePredicateField = ( ((value)).indexOf((operand)) === -1 ); default: - exhaustiveCheck(operator, false); return false; } }; @@ -250,7 +243,8 @@ export const establishRelationAndKeys = ( const fieldAttribute = model.fields[attr]; if ( typeof fieldAttribute.type === 'object' && - 'model' in fieldAttribute.type + 'model' in fieldAttribute.type && + fieldAttribute.association ) { const connectionType = fieldAttribute.association.connectionType; relationship[mKey].relationTypes.push({ @@ -261,7 +255,10 @@ export const establishRelationAndKeys = ( associatedWith: fieldAttribute.association['associatedWith'], }); - if (connectionType === 'BELONGS_TO') { + if ( + connectionType === 'BELONGS_TO' && + fieldAttribute.association['targetName'] + ) { relationship[mKey].indexes.push( fieldAttribute.association['targetName'] ); @@ -392,7 +389,7 @@ export const traverseModel = ( // // Intentionally blank // break; // default: - // exhaustiveCheck(rItem.relationType); + // throw new Error(`Invalid ${rItem.relationType}`); // break; // } // }); @@ -407,11 +404,11 @@ export const traverseModel = ( if (!topologicallySortedModels.has(namespace)) { topologicallySortedModels.set( namespace, - Array.from(namespace.modelTopologicalOrdering.keys()) + Array.from(namespace.modelTopologicalOrdering?.keys() || []) ); } - const sortedModels = topologicallySortedModels.get(namespace); + const sortedModels = topologicallySortedModels.get(namespace) || []; result.sort((a, b) => { return ( @@ -426,7 +423,7 @@ export const getIndex = (rel: RelationType[], src: string): string => { let index = ''; rel.some((relItem: RelationType) => { if (relItem.modelName === src) { - index = relItem.targetName; + index = relItem.targetName || ''; } }); return index; @@ -437,7 +434,7 @@ export const getIndexFromAssociation = ( src: string ): string => { const index = indexes.find(idx => idx === src); - return index; + return index || ''; }; export enum NAMESPACES { @@ -495,9 +492,9 @@ export const isPrivateMode = () => { }); }; -const randomBytes = (nBytes: number): Buffer => { +function randomBytes(nBytes: number): Buffer { return Buffer.from(new WordArray().random(nBytes).toString(), 'hex'); -}; +} const prng = () => randomBytes(1).readUInt8(0) / 0xff; export function monotonicUlidFactory(seed?: number): ULID { const ulid = monotonicFactory(prng); @@ -717,7 +714,7 @@ export async function asyncFilter( items: T[], matches: (item: T) => Promise ): Promise { - const results = []; + const results: T[] = []; for (const item of items) { if (await matches(item)) { results.push(item); @@ -840,7 +837,7 @@ export class DeferredCallbackResolver { this.raceInFlight = false; this.limitPromise = new DeferredPromise(); - return winner; + return winner!; } }