diff --git a/packages/api-graphql/__tests__/__snapshots__/generateClient.test.ts.snap b/packages/api-graphql/__tests__/__snapshots__/generateClient.test.ts.snap index 2cedf31b865..dc5b88eee18 100644 --- a/packages/api-graphql/__tests__/__snapshots__/generateClient.test.ts.snap +++ b/packages/api-graphql/__tests__/__snapshots__/generateClient.test.ts.snap @@ -4613,6 +4613,102 @@ exports[`generateClient graphql default auth default iam produces expected signi ] `; +exports[`generateClient index queries PK and SK index query 1`] = ` +[ + [ + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $limit: Int, $nextToken: String) { + listByDescriptionAndViewCount( + description: $description + viewCount: $viewCount + filter: $filter + limit: $limit + nextToken: $nextToken + ) { + items { + id + title + description + viewCount + status + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "description": "something something", + "viewCount": { + "gt": 4, + }, + }, + }, + "headers": { + "Authorization": "amplify-config-auth-token", + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`generateClient index queries PK-only index query 1`] = ` +[ + [ + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($title: String!, $filter: ModelSecondaryIndexModelFilterInput, $limit: Int, $nextToken: String) { + listByTitle( + title: $title + filter: $filter + limit: $limit + nextToken: $nextToken + ) { + items { + id + title + description + viewCount + status + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "title": "Hello World", + }, + }, + "headers": { + "Authorization": "amplify-config-auth-token", + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + exports[`generateClient observeQuery can paginate through initial results 1`] = ` [ [ diff --git a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.js b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.js index d00b387db1a..390e84d175f 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.js +++ b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.js @@ -959,6 +959,108 @@ const amplifyConfig = { sortKeyFieldNames: [], }, }, + SecondaryIndexModel: { + name: 'SecondaryIndexModel', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + title: { + name: 'title', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + description: { + name: 'description', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + viewCount: { + name: 'viewCount', + isArray: false, + type: 'Int', + isRequired: false, + attributes: [], + }, + status: { + name: 'status', + isArray: false, + type: { + enum: 'Status', + }, + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: true, + attributes: [], + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: true, + attributes: [], + }, + }, + syncable: true, + pluralName: 'SecondaryIndexModels', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'key', + properties: { + fields: ['id'], + }, + }, + { + type: 'key', + properties: { + name: 'secondaryIndexModelsByTitle', + queryField: 'listByTitle', + fields: ['title'], + }, + }, + { + type: 'key', + properties: { + name: 'secondaryIndexModelsByDescriptionAndViewCount', + queryField: 'listByDescriptionAndViewCount', + fields: ['description', 'viewCount'], + }, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + primaryKeyInfo: { + isCustomPrimaryKey: false, + primaryKeyFieldName: 'id', + sortKeyFieldNames: [], + }, + }, }, enums: { Status: { diff --git a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts index bc98a3fb7d8..3cd50241cc5 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts @@ -70,6 +70,17 @@ const schema = a.schema({ CommunityPollVote: a .model({ id: a.id().required() }) .authorization([a.allow.public('apiKey'), a.allow.owner()]), + SecondaryIndexModel: a + .model({ + title: a.string(), + description: a.string(), + viewCount: a.integer(), + status: a.enum(['draft', 'pending', 'published']), + }) + .secondaryIndexes([ + a.index('title'), + a.index('description').sortKeys(['viewCount']), + ]), }); export type Schema = ClientSchema; diff --git a/packages/api-graphql/__tests__/generateClient.test.ts b/packages/api-graphql/__tests__/generateClient.test.ts index 9951586e20e..d0c0b10d094 100644 --- a/packages/api-graphql/__tests__/generateClient.test.ts +++ b/packages/api-graphql/__tests__/generateClient.test.ts @@ -49,7 +49,7 @@ function makeAppSyncStreams() { >; const spy = jest.fn(request => { const matchedType = (request.query as string).match( - /on(Create|Update|Delete)/ + /on(Create|Update|Delete)/, ); if (matchedType) { return new Observable(subscriber => { @@ -260,7 +260,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -290,7 +290,7 @@ describe('generateClient', () => { name: 'some name', description: 'something something', tags: ['one', 'two', 'three'], - }) + }), ); }); @@ -325,7 +325,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -406,7 +406,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some other name', description: 'something something', - }) + }), ); }); @@ -436,7 +436,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -599,7 +599,7 @@ describe('generateClient', () => { id: 'note-id', owner: 'wirejobviously', body: 'some body', - }) + }), ); }); @@ -645,7 +645,7 @@ describe('generateClient', () => { id: 'note-id', owner: 'wirejobviously', body: 'some body', - }) + }), ); }); @@ -691,7 +691,7 @@ describe('generateClient', () => { id: 'note-id', owner: 'wirejobviously', body: 'some body', - }) + }), ); }); @@ -733,7 +733,7 @@ describe('generateClient', () => { id: 'todo-id', name: 'some name', description: 'something something', - }) + }), ); }); @@ -773,7 +773,7 @@ describe('generateClient', () => { __typename: 'TodoMetadata', id: 'meta-id', data: '{"field":"value"}', - }) + }), ); }); }); @@ -820,7 +820,7 @@ describe('generateClient', () => { }, { authMode: 'userPool', - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -887,7 +887,7 @@ describe('generateClient', () => { id: 'some-id', name: 'some other name', }, - { authMode: 'userPool' } + { authMode: 'userPool' }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -910,7 +910,7 @@ describe('generateClient', () => { { id: 'some-id', }, - { authMode: 'userPool' } + { authMode: 'userPool' }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -942,7 +942,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -979,7 +979,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1016,7 +1016,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1044,7 +1044,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = await client.models.Todo.get( { id: 'todo-id' }, - { authMode: 'userPool' } + { authMode: 'userPool' }, ); const getChildNotesSpy = mockApiResponse({ @@ -1085,7 +1085,7 @@ describe('generateClient', () => { { id: 'note-id' }, { authMode: 'userPool', - } + }, ); const getChildNotesSpy = mockApiResponse({ @@ -1123,7 +1123,7 @@ describe('generateClient', () => { { id: 'todo-id' }, { authMode: 'userPool', - } + }, ); const getChildMetaSpy = mockApiResponse({ @@ -1160,7 +1160,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = await client.models.Todo.get( { id: 'todo-id' }, - { authMode: 'userPool' } + { authMode: 'userPool' }, ); const getChildNotesSpy = mockApiResponse({ @@ -1201,7 +1201,7 @@ describe('generateClient', () => { { id: 'note-id' }, { authMode: 'userPool', - } + }, ); const getChildNotesSpy = mockApiResponse({ @@ -1239,7 +1239,7 @@ describe('generateClient', () => { { id: 'todo-id' }, { authMode: 'userPool', - } + }, ); const getChildMetaSpy = mockApiResponse({ @@ -1303,7 +1303,7 @@ describe('generateClient', () => { { authMode: 'lambda', authToken: 'some-token', - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -1324,7 +1324,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); await client.models.Todo.get( { id: 'asdf' }, - { authMode: 'lambda', authToken: 'some-token' } + { authMode: 'lambda', authToken: 'some-token' }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -1374,7 +1374,7 @@ describe('generateClient', () => { id: 'some-id', name: 'some other name', }, - { authMode: 'lambda', authToken: 'some-token' } + { authMode: 'lambda', authToken: 'some-token' }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -1397,7 +1397,7 @@ describe('generateClient', () => { { id: 'some-id', }, - { authMode: 'lambda', authToken: 'some-token' } + { authMode: 'lambda', authToken: 'some-token' }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -1430,7 +1430,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1468,7 +1468,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1506,7 +1506,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1534,7 +1534,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = await client.models.Todo.get( { id: 'todo-id' }, - { authMode: 'lambda', authToken: 'some-token' } + { authMode: 'lambda', authToken: 'some-token' }, ); const getChildNotesSpy = mockApiResponse({ @@ -1576,7 +1576,7 @@ describe('generateClient', () => { { authMode: 'lambda', authToken: 'some-token', - } + }, ); const getChildNotesSpy = mockApiResponse({ @@ -1615,7 +1615,7 @@ describe('generateClient', () => { { authMode: 'lambda', authToken: 'some-token', - } + }, ); const getChildMetaSpy = mockApiResponse({ @@ -1652,7 +1652,7 @@ describe('generateClient', () => { const client = generateClient({ amplify: Amplify }); const { data } = await client.models.Todo.get( { id: 'todo-id' }, - { authMode: 'userPool' } + { authMode: 'userPool' }, ); const getChildNotesSpy = mockApiResponse({ @@ -1693,7 +1693,7 @@ describe('generateClient', () => { { id: 'note-id' }, { authMode: 'userPool', - } + }, ); const getChildNotesSpy = mockApiResponse({ @@ -1731,7 +1731,7 @@ describe('generateClient', () => { { id: 'todo-id' }, { authMode: 'userPool', - } + }, ); const getChildMetaSpy = mockApiResponse({ @@ -1920,7 +1920,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1958,7 +1958,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -1996,7 +1996,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -2406,7 +2406,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -2445,7 +2445,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -2484,7 +2484,7 @@ describe('generateClient', () => { expect.objectContaining({ authenticationType: 'lambda', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); done(); }, @@ -2767,7 +2767,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -2804,7 +2804,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -2843,7 +2843,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -2875,7 +2875,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -2888,7 +2888,7 @@ describe('generateClient', () => { 'client-header': 'should not exist', }), }), - }) + }), ); expect(data).toEqual( @@ -2898,7 +2898,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -2930,7 +2930,7 @@ describe('generateClient', () => { headers: async () => ({ 'request-header-function': 'should return this header', }), - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -2942,7 +2942,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -2976,7 +2976,7 @@ describe('generateClient', () => { 'rq-qs': requestOptions?.queryString || 'should-not-be-present', 'rq-method': requestOptions?.method || 'should-not-be-present', }), - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -2988,7 +2988,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3021,7 +3021,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3049,7 +3049,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3061,7 +3061,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3101,7 +3101,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3144,7 +3144,7 @@ describe('generateClient', () => { query: expect.stringMatching(/^\s*nextToken\s*$/m), }), }), - }) + }), ); expect(data.length).toBe(1); @@ -3155,7 +3155,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3191,7 +3191,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some other name', description: 'something something', - }) + }), ); }); @@ -3222,7 +3222,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3234,7 +3234,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some other name', description: 'something something', - }) + }), ); }); @@ -3289,7 +3289,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3301,7 +3301,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3342,7 +3342,7 @@ describe('generateClient', () => { spy, 'onCreateNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -3479,7 +3479,7 @@ describe('generateClient', () => { spy, 'onUpdateNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -3528,7 +3528,7 @@ describe('generateClient', () => { spy, 'onDeleteNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -3589,7 +3589,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3626,7 +3626,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3655,7 +3655,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3667,7 +3667,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3700,7 +3700,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3728,7 +3728,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3740,7 +3740,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3780,7 +3780,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3823,7 +3823,7 @@ describe('generateClient', () => { query: expect.stringMatching(/^\s*nextToken\s*$/m), }), }), - }) + }), ); expect(data.length).toBe(1); @@ -3834,7 +3834,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3870,7 +3870,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some other name', description: 'something something', - }) + }), ); }); @@ -3901,7 +3901,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3913,7 +3913,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some other name', description: 'something something', - }) + }), ); }); @@ -3948,7 +3948,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -3978,7 +3978,7 @@ describe('generateClient', () => { headers: { 'request-header': 'should exist', }, - } + }, ); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -3990,7 +3990,7 @@ describe('generateClient', () => { owner: 'wirejobviously', name: 'some name', description: 'something something', - }) + }), ); }); @@ -4032,7 +4032,7 @@ describe('generateClient', () => { spy, 'onCreateNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -4082,7 +4082,7 @@ describe('generateClient', () => { spy, 'onUpdateNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -4132,7 +4132,7 @@ describe('generateClient', () => { spy, 'onDeleteNote', graphqlVariables, - customHeaders + customHeaders, ); expect(value).toEqual(expect.objectContaining(noteToSend)); done(); @@ -4410,7 +4410,7 @@ describe('generateClient', () => { callSequence.push('list'); resolve(result); }, 15); - }) + }), ); const { streams, spy } = makeAppSyncStreams(); @@ -4493,7 +4493,7 @@ describe('generateClient', () => { callSequence.push('list'); resolve(result); }, 15); - }) + }), ); const { streams, spy } = makeAppSyncStreams(); @@ -4569,7 +4569,7 @@ describe('generateClient', () => { callSequence.push('list'); resolve(result); }, 15); - }) + }), ); const { streams, spy } = makeAppSyncStreams(); @@ -4766,7 +4766,7 @@ describe('generateClient', () => { // configured fixture value is expected be `apiKey` for this test authenticationType: 'apiKey', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); } done(); @@ -4793,7 +4793,7 @@ describe('generateClient', () => { query: expect.stringContaining(op), authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); } done(); @@ -4824,7 +4824,7 @@ describe('generateClient', () => { authenticationType: 'lambda', authToken: 'some-token', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); } done(); @@ -4854,7 +4854,7 @@ describe('generateClient', () => { query: expect.stringContaining(op), authenticationType: 'userPool', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); } done(); @@ -4886,7 +4886,7 @@ describe('generateClient', () => { authenticationType: 'lambda', authToken: 'some-token', }), - USER_AGENT_DETAILS + USER_AGENT_DETAILS, ); } done(); @@ -5010,4 +5010,85 @@ describe('generateClient', () => { expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); }); }); + + describe('index queries', () => { + beforeEach(() => { + jest.clearAllMocks(); + Amplify.configure(configFixture); + }); + + test('PK-only index query', async () => { + const spy = mockApiResponse({ + data: { + listByTitle: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + title: 'Hello World', + description: 'something something', + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = await client.models.SecondaryIndexModel.listByTitle({ + title: 'Hello World', + }); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + + expect(data.length).toBe(1); + expect(data[0]).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + title: 'Hello World', + description: 'something something', + }), + ); + }); + + test('PK and SK index query', async () => { + const spy = mockApiResponse({ + data: { + listByDescriptionAndViewCount: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + title: 'Hello World', + description: 'something something', + viewCount: 5, + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = + await client.models.SecondaryIndexModel.listByDescriptionAndViewCount({ + description: 'something something', + viewCount: { gt: 4 }, + }); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + + expect(data.length).toBe(1); + expect(data[0]).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + title: 'Hello World', + description: 'something something', + viewCount: 5, + }), + ); + }); + }); }); diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index e860fa3abcd..d3282890cef 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -71,7 +71,7 @@ }, "homepage": "https://aws-amplify.github.io/", "devDependencies": { - "@aws-amplify/data-schema": "^0.12.11", + "@aws-amplify/data-schema": "^0.13.0", "typescript": "5.0.2", "@rollup/plugin-typescript": "11.1.5", "rollup": "^4.9.6" @@ -86,7 +86,7 @@ "dependencies": { "@aws-amplify/api-rest": "4.0.14", "@aws-amplify/core": "6.0.14", - "@aws-amplify/data-schema-types": "^0.6.10", + "@aws-amplify/data-schema-types": "^0.7.0", "@aws-sdk/types": "3.387.0", "graphql": "15.8.0", "rxjs": "^7.8.1", diff --git a/packages/api-graphql/src/internals/APIClient.ts b/packages/api-graphql/src/internals/APIClient.ts index 43820cdc185..08e4c7e7b86 100644 --- a/packages/api-graphql/src/internals/APIClient.ts +++ b/packages/api-graphql/src/internals/APIClient.ts @@ -24,6 +24,7 @@ import { } from '../types'; import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { CustomHeaders } from '@aws-amplify/data-schema-types'; +import type { IndexMeta } from './operations/indexQuery'; type LazyLoadOptions = { authMode?: GraphQLAuthMode; @@ -51,7 +52,7 @@ export const flattenItems = (obj: Record): Record => { if (typeof value === 'object' && !Array.isArray(value) && value !== null) { if (value.items !== undefined) { res[prop] = value.items.map((item: Record) => - flattenItems(item) + flattenItems(item), ); return; } @@ -73,7 +74,7 @@ export function initializeModel( modelIntrospection: ModelIntrospectionSchema, authMode: GraphQLAuthMode | undefined, authToken: string | undefined, - context = false + context = false, ): any[] { const introModel = modelIntrospection.models[modelName]; const introModelFields = introModel.fields; @@ -122,13 +123,13 @@ export function initializeModel( return (acc[curVal] = record[curVal]); } }, - {} + {}, ); if (context) { initializedRelationalFields[fieldName] = ( contextSpec: AmplifyServer.ContextSpec, - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[targetNames[0]]) { return ( @@ -142,14 +143,14 @@ export function initializeModel( { authMode: options?.authMode || authMode, authToken: options?.authToken || authToken, - } + }, ); } return undefined; }; } else { initializedRelationalFields[fieldName] = ( - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[targetNames[0]]) { return (client as V6Client>).models[ @@ -162,7 +163,7 @@ export function initializeModel( { authMode: options?.authMode || authMode, authToken: options?.authToken || authToken, - } + }, ); } return undefined; @@ -194,13 +195,13 @@ export function initializeModel( } return { [field]: { eq: record[parentSK[idx - 1]] } }; - } + }, ); if (context) { initializedRelationalFields[fieldName] = ( contextSpec: AmplifyServer.ContextSpec, - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[parentPk]) { return ( @@ -217,7 +218,7 @@ export function initializeModel( }; } else { initializedRelationalFields[fieldName] = ( - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[parentPk]) { return (client as V6Client>).models[ @@ -244,13 +245,13 @@ export function initializeModel( } return { [field]: { eq: record[parentSK[idx - 1]] } }; - } + }, ); if (context) { initializedRelationalFields[fieldName] = ( contextSpec: AmplifyServer.ContextSpec, - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[parentPk]) { return ( @@ -267,7 +268,7 @@ export function initializeModel( }; } else { initializedRelationalFields[fieldName] = ( - options?: LazyLoadOptions + options?: LazyLoadOptions, ) => { if (record[parentPk]) { return (client as V6Client>).models[ @@ -295,16 +296,17 @@ export function initializeModel( } export const graphQLOperationsInfo = { - CREATE: { operationPrefix: 'create' as const, usePlural: false }, - READ: { operationPrefix: 'get' as const, usePlural: false }, - UPDATE: { operationPrefix: 'update' as const, usePlural: false }, - DELETE: { operationPrefix: 'delete' as const, usePlural: false }, - LIST: { operationPrefix: 'list' as const, usePlural: true }, - ONCREATE: { operationPrefix: 'onCreate' as const, usePlural: false }, - ONUPDATE: { operationPrefix: 'onUpdate' as const, usePlural: false }, - ONDELETE: { operationPrefix: 'onDelete' as const, usePlural: false }, - OBSERVE_QUERY: { operationPrefix: 'observeQuery' as const, usePlural: false }, -}; + CREATE: { operationPrefix: 'create', usePlural: false }, + READ: { operationPrefix: 'get', usePlural: false }, + UPDATE: { operationPrefix: 'update', usePlural: false }, + DELETE: { operationPrefix: 'delete', usePlural: false }, + LIST: { operationPrefix: 'list', usePlural: true }, + INDEX_QUERY: { operationPrefix: '', usePlural: false }, + ONCREATE: { operationPrefix: 'onCreate', usePlural: false }, + ONUPDATE: { operationPrefix: 'onUpdate', usePlural: false }, + ONDELETE: { operationPrefix: 'onDelete', usePlural: false }, + OBSERVE_QUERY: { operationPrefix: 'observeQuery', usePlural: false }, +} as const; export type ModelOperation = keyof typeof graphQLOperationsInfo; type OperationPrefix = @@ -322,7 +324,7 @@ function defaultSelectionSetForModel(modelDefinition: SchemaModel): string[] { ({ type, name }) => (typeof type === 'string' || (typeof type === 'object' && typeof type?.enum === 'string')) && - name + name, ) .filter(Boolean); @@ -357,11 +359,11 @@ const FIELD_IR = ''; export function customSelectionSetToIR( modelDefinitions: SchemaModels, modelName: string, - selectionSet: string[] + selectionSet: string[], ): Record { const dotNotationToObject = ( path: string, - modelName: string + modelName: string, ): Record => { const [fieldName, ...rest] = path.split('.'); @@ -373,7 +375,8 @@ export function customSelectionSetToIR( const nested = rest[0]; const modelDefinition = modelDefinitions[modelName]; const modelFields = modelDefinition.fields; - const relatedModel = (modelFields[fieldName]?.type as ModelFieldType)?.model; + const relatedModel = (modelFields[fieldName]?.type as ModelFieldType) + ?.model; if (!relatedModel) { // TODO: may need to change this to support custom types @@ -413,15 +416,15 @@ export function customSelectionSetToIR( (resultObj, path) => deepMergeSelectionSetObjects( dotNotationToObject(path, modelName), - resultObj + resultObj, ), - {} as Record + {} as Record, ); } const defaultSelectionSetIR = (relatedModelDefinition: SchemaModel) => { const defaultSelectionSet = defaultSelectionSetForModel( - relatedModelDefinition + relatedModelDefinition, ); const reduced = defaultSelectionSet.reduce( @@ -429,7 +432,7 @@ const defaultSelectionSetIR = (relatedModelDefinition: SchemaModel) => { acc[curVal] = FIELD_IR; return acc; }, - {} + {}, ); return reduced; @@ -451,7 +454,7 @@ const defaultSelectionSetIR = (relatedModelDefinition: SchemaModel) => { * `'id comments { items { post { id } } }'` */ export function selectionSetIRToString( - obj: Record + obj: Record, ): string { const res: string[] = []; @@ -467,7 +470,7 @@ export function selectionSetIRToString( '{', selectionSetIRToString(value.items), '}', - '}' + '}', ); } else { res.push(fieldName, '{', selectionSetIRToString(value), '}'); @@ -488,7 +491,7 @@ export function selectionSetIRToString( */ function deepMergeSelectionSetObjects>( source: T, - target: T + target: T, ) { const isObject = (obj: any) => obj && typeof obj === 'object'; @@ -509,7 +512,7 @@ function deepMergeSelectionSetObjects>( export function generateSelectionSet( modelDefinitions: SchemaModels, modelName: string, - selectionSet?: string[] + selectionSet?: string[], ) { const modelDefinition = modelDefinitions[modelName]; @@ -520,7 +523,7 @@ export function generateSelectionSet( const selSetIr = customSelectionSetToIR( modelDefinitions, modelName, - selectionSet + selectionSet, ); const selSetString = selectionSetIRToString(selSetIr); @@ -531,7 +534,8 @@ export function generateGraphQLDocument( modelDefinitions: SchemaModels, modelName: string, modelOperation: ModelOperation, - listArgs?: ListArgs + listArgs?: ListArgs | QueryArgs, + indexMeta?: IndexMeta, ): string { const modelDefinition = modelDefinitions[modelName]; @@ -549,7 +553,33 @@ export function generateGraphQLDocument( const { operationPrefix, usePlural } = graphQLOperationsInfo[modelOperation]; const { selectionSet } = listArgs || {}; - const graphQLFieldName = `${operationPrefix}${usePlural ? pluralName : name}`; + + let graphQLFieldName; + let indexQueryArgs: Record; + + if (operationPrefix) { + graphQLFieldName = `${operationPrefix}${usePlural ? pluralName : name}`; + } else if (indexMeta) { + const { queryField, pk, sk = [] } = indexMeta; + graphQLFieldName = queryField; + + const skQueryArgs = sk.reduce((acc: Record, fieldName) => { + const fieldType = fields[fieldName].type; + acc[fieldName] = `Model${fieldType}KeyConditionInput`; + return acc; + }, {}); + + indexQueryArgs = { + [pk]: `${fields[pk].type}!`, + ...skQueryArgs, + }; + } else { + throw new Error( + 'Error generating GraphQL Document - invalid operation name', + ); + } + + console.log('indexQueryArgs', indexQueryArgs!); let graphQLOperationType: 'mutation' | 'query' | 'subscription' | undefined; let graphQLSelectionSet: string | undefined; @@ -558,7 +588,7 @@ export function generateGraphQLDocument( const selectionSetFields = generateSelectionSet( modelDefinitions, modelName, - selectionSet + selectionSet as ListArgs['selectionSet'], ); switch (modelOperation) { @@ -582,7 +612,7 @@ export function generateGraphQLDocument( return acc; }, - {} + {}, ) : { [primaryKeyFieldName]: `${fields[primaryKeyFieldName].type}!`, @@ -598,6 +628,17 @@ export function generateGraphQLDocument( graphQLOperationType ?? (graphQLOperationType = 'query'); graphQLSelectionSet ?? (graphQLSelectionSet = `items { ${selectionSetFields} } nextToken __typename`); + case 'INDEX_QUERY': + graphQLArguments ?? + (graphQLArguments = { + ...indexQueryArgs!, + filter: `Model${name}FilterInput`, + limit: 'Int', + nextToken: 'String', + }); + graphQLOperationType ?? (graphQLOperationType = 'query'); + graphQLSelectionSet ?? + (graphQLSelectionSet = `items { ${selectionSetFields} } nextToken __typename`); case 'ONCREATE': case 'ONUPDATE': case 'ONDELETE': @@ -611,20 +652,20 @@ export function generateGraphQLDocument( case 'OBSERVE_QUERY': default: throw new Error( - 'Internal error: Attempted to generate graphql document for observeQuery. Please report this error.' + 'Internal error: Attempted to generate graphql document for observeQuery. Please report this error.', ); } const graphQLDocument = `${graphQLOperationType}${ graphQLArguments ? `(${Object.entries(graphQLArguments).map( - ([fieldName, type]) => `\$${fieldName}: ${type}` + ([fieldName, type]) => `\$${fieldName}: ${type}`, )})` : '' } { ${graphQLFieldName}${ graphQLArguments ? `(${Object.keys(graphQLArguments).map( - fieldName => `${fieldName}: \$${fieldName}` + fieldName => `${fieldName}: \$${fieldName}`, )})` : '' } { ${graphQLSelectionSet} } }`; @@ -636,7 +677,8 @@ export function buildGraphQLVariables( modelDefinition: SchemaModel, operation: ModelOperation, arg: QueryArgs | undefined, - modelIntrospection: ModelIntrospectionSchema + modelIntrospection: ModelIntrospectionSchema, + indexMeta?: IndexMeta, ): object { const { fields, @@ -664,12 +706,16 @@ export function buildGraphQLVariables( input: arg ? Object.fromEntries( Object.entries( - normalizeMutationInput(arg, modelDefinition, modelIntrospection) + normalizeMutationInput( + arg, + modelDefinition, + modelIntrospection, + ), ).filter(([fieldName]) => { const { isReadOnly } = fields[fieldName]; return !isReadOnly; - }) + }), ) : {}, }; @@ -685,7 +731,7 @@ export function buildGraphQLVariables( return acc; }, - {} + {}, ) : { [primaryKeyFieldName]: arg[primaryKeyFieldName] }; } @@ -695,6 +741,25 @@ export function buildGraphQLVariables( } break; case 'LIST': + if (arg?.filter) { + variables.filter = arg.filter; + } + if (arg?.nextToken) { + variables.nextToken = arg.nextToken; + } + if (arg?.limit) { + variables.limit = arg.limit; + } + break; + case 'INDEX_QUERY': + const { pk, sk = [] } = indexMeta!; + + variables[pk] = arg![pk]; + + for (const skField of sk) { + variables[skField] = arg![skField]; + } + if (arg?.filter) { variables.filter = arg.filter; } @@ -714,7 +779,7 @@ export function buildGraphQLVariables( break; case 'OBSERVE_QUERY': throw new Error( - 'Internal error: Attempted to build variables for observeQuery. Please report this error.' + 'Internal error: Attempted to build variables for observeQuery. Please report this error.', ); break; default: @@ -742,7 +807,7 @@ export function buildGraphQLVariables( export function normalizeMutationInput( mutationInput: QueryArgs, model: SchemaModel, - modelIntrospection: ModelIntrospectionSchema + modelIntrospection: ModelIntrospectionSchema, ): QueryArgs { const { fields } = model; @@ -803,7 +868,7 @@ export function normalizeMutationInput( */ export function authModeParams( client: ClientWithModels, - options: AuthModeParams = {} + options: AuthModeParams = {}, ): AuthModeParams { return { authMode: options.authMode || client[__authMode], @@ -819,7 +884,7 @@ export function authModeParams( */ export function getCustomHeaders( client: V6Client | ClientWithModels, - requestHeaders?: CustomHeaders + requestHeaders?: CustomHeaders, ): CustomHeaders { let headers: CustomHeaders = client[__headers] || {}; diff --git a/packages/api-graphql/src/internals/generateModelsProperty.ts b/packages/api-graphql/src/internals/generateModelsProperty.ts index ec7a9e009cf..5db11ca4d1a 100644 --- a/packages/api-graphql/src/internals/generateModelsProperty.ts +++ b/packages/api-graphql/src/internals/generateModelsProperty.ts @@ -6,14 +6,18 @@ import { ClientGenerationParams } from './types'; import { V6Client, __authMode, __authToken } from '../types'; import { listFactory } from './operations/list'; +import { indexQueryFactory } from './operations/indexQuery'; import { getFactory } from './operations/get'; import { subscriptionFactory } from './operations/subscription'; import { observeQueryFactory } from './operations/observeQuery'; -import { ModelIntrospectionSchema } from '@aws-amplify/core/internals/utils'; +import { + ModelIntrospectionSchema, + SchemaModel, +} from '@aws-amplify/core/internals/utils'; export function generateModelsProperty = never>( client: V6Client>, - params: ClientGenerationParams + params: ClientGenerationParams, ): ModelTypes { const models = {} as any; const config = params.amplify.getConfig(); @@ -51,14 +55,14 @@ export function generateModelsProperty = never>( models[name][operationPrefix] = listFactory( client, modelIntrospection, - model + model, ); } else if (SUBSCRIPTION_OPS.includes(operation)) { models[name][operationPrefix] = subscriptionFactory( client, modelIntrospection, model, - operation + operation, ); } else if (operation === 'OBSERVE_QUERY') { models[name][operationPrefix] = observeQueryFactory(models, model); @@ -67,11 +71,46 @@ export function generateModelsProperty = never>( client, modelIntrospection, model, - operation + operation, ); } - } + }, ); + + const getSecondaryIndexesFromSchemaModel = (model: SchemaModel) => { + const idxs = model.attributes + ?.filter( + attr => + attr.type === 'key' && + // presence of `name` property distinguishes GSI from primary index + attr.properties?.name && + attr.properties?.queryField && + attr.properties?.fields.length > 0, + ) + .map(attr => { + const queryField: string = attr.properties?.queryField; + const [pk, ...sk] = attr.properties?.fields; + + return { + queryField, + pk, + sk, + }; + }); + + return idxs || []; + }; + + const secondaryIdxs = getSecondaryIndexesFromSchemaModel(model); + + for (const idx of secondaryIdxs) { + models[name][idx.queryField] = indexQueryFactory( + client, + modelIntrospection, + model, + idx, + ); + } } return models; diff --git a/packages/api-graphql/src/internals/operations/indexQuery.ts b/packages/api-graphql/src/internals/operations/indexQuery.ts new file mode 100644 index 00000000000..2a2fdad01b8 --- /dev/null +++ b/packages/api-graphql/src/internals/operations/indexQuery.ts @@ -0,0 +1,164 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; +import { + initializeModel, + generateGraphQLDocument, + buildGraphQLVariables, + flattenItems, + authModeParams, + getCustomHeaders, +} from '../APIClient'; +import { + AuthModeParams, + ClientWithModels, + ListArgs, + V6Client, + V6ClientSSRRequest, + GraphQLResult, + QueryArgs, +} from '../../types'; +import { + ModelIntrospectionSchema, + SchemaModel, +} from '@aws-amplify/core/internals/utils'; + +export type IndexMeta = { + queryField: string; + pk: string; + sk?: string[]; +}; + +export function indexQueryFactory( + client: ClientWithModels, + modelIntrospection: ModelIntrospectionSchema, + model: SchemaModel, + indexMeta: IndexMeta, + context = false, +) { + const indexQueryWithContext = async ( + contextSpec: AmplifyServer.ContextSpec, + args: QueryArgs, + options?: ListArgs, + ) => { + return _indexQuery( + client, + modelIntrospection, + model, + indexMeta, + { + ...args, + ...options, + }, + contextSpec, + ); + }; + + const indexQuery = async (args: QueryArgs, options?: ListArgs) => { + return _indexQuery(client, modelIntrospection, model, indexMeta, { + ...args, + ...options, + }); + }; + + return context ? indexQueryWithContext : indexQuery; +} + +async function _indexQuery( + client: ClientWithModels, + modelIntrospection: ModelIntrospectionSchema, + model: SchemaModel, + indexMeta: IndexMeta, + args?: ListArgs & AuthModeParams, + contextSpec?: AmplifyServer.ContextSpec, +) { + const { name } = model; + + const query = generateGraphQLDocument( + modelIntrospection.models, + name, + 'INDEX_QUERY', + args, + indexMeta, + ); + const variables = buildGraphQLVariables( + model, + 'INDEX_QUERY', + args, + modelIntrospection, + indexMeta, + ); + + try { + const auth = authModeParams(client, args); + + const headers = getCustomHeaders(client, args?.headers); + + const { data, extensions } = !!contextSpec + ? ((await (client as V6ClientSSRRequest>).graphql( + contextSpec, + { + ...auth, + query, + variables, + }, + headers, + )) as GraphQLResult) + : ((await (client as V6Client>).graphql( + { + ...auth, + query, + variables, + }, + headers, + )) as GraphQLResult); + + // flatten response + if (data !== undefined) { + const [key] = Object.keys(data); + + if (data[key].items) { + const flattenedResult = flattenItems(data)[key]; + + // don't init if custom selection set + if (args?.selectionSet) { + return { + data: flattenedResult, + nextToken: data[key].nextToken, + extensions, + }; + } else { + const initialized = initializeModel( + client, + name, + flattenedResult, + modelIntrospection, + auth.authMode, + auth.authToken, + !!contextSpec, + ); + + return { + data: initialized, + nextToken: data[key].nextToken, + extensions, + }; + } + } + + return { + data: data[key], + nextToken: data[key].nextToken, + extensions, + }; + } + } catch (error: any) { + if (error.errors) { + // graphql errors pass through + return error as any; + } else { + // non-graphql errors re re-thrown + throw error; + } + } +} diff --git a/yarn.lock b/yarn.lock index caf6780a377..a4b302b0b64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,17 +15,17 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aws-amplify/data-schema-types@*", "@aws-amplify/data-schema-types@^0.6.10": - version "0.6.10" - resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema-types/-/data-schema-types-0.6.10.tgz#e208e57dd2e7de0b9d479d19c1d8459b578df506" - integrity sha512-o893k1tNJ0iR9w9Q/jymhSQZmgNdH/L5tz+RXyWcQO+qgN3XhGaayqjYKcQ22XkB1pBgGkXtRLO6Jg74KriBWg== +"@aws-amplify/data-schema-types@*", "@aws-amplify/data-schema-types@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema-types/-/data-schema-types-0.7.0.tgz#667d29bf1be5c3d1b97abc0d3e7483dca223c4d4" + integrity sha512-dPxUdbHn/wc99OVJZMvMbGoNV+2siRyiHnlhgr20MF+l9Ek3vz0EKbzLZhEq61SNt/hRNY5rC9mYjepFoQBzzg== dependencies: rxjs "^7.8.1" -"@aws-amplify/data-schema@^0.12.11": - version "0.12.11" - resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-0.12.11.tgz#bc43ada620a2b7311c2523891f96a0bc66ca50c3" - integrity sha512-wc7bHMrmw1+gFcY36BaZsPd08vJisfCZjjYzuXREitMHmy97HYbvva22nPbDUciQWTk+87InYflYoS5NJnKspg== +"@aws-amplify/data-schema@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema/-/data-schema-0.13.0.tgz#0fa4a64d91969a505f746b17fc89674f4782ccee" + integrity sha512-qFjI1VYbj+nLR5Zkn5MrS/tcTdrUuWfVuuuUz1v3Epn4AE6s+j+AMeNB+mQ7YKraSHMygM82VhuegBKtgYEz2g== dependencies: "@aws-amplify/data-schema-types" "*"