From a620a2c09bd63461b957861799d0641cd154f867 Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Thu, 1 Feb 2024 09:03:29 -0800 Subject: [PATCH] fix(api-graphql): generate client.models on Hub core:configure event --- .../internals/generateClient.test.ts | 61 +++++++++++++++---- .../src/internals/generateClient.ts | 47 +++++++++++++- .../src/internals/generateModelsProperty.ts | 17 +----- packages/core/src/index.ts | 1 + packages/core/src/singleton/types.ts | 7 ++- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 4d02c4c1215..9ff372ce994 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -1,5 +1,5 @@ import * as raw from '../../src'; -import { Amplify, AmplifyClassV6 } from '@aws-amplify/core'; +import { Amplify, AmplifyClassV6, Hub } from '@aws-amplify/core'; import { generateClient } from '../../src/internals'; import configFixture from '../fixtures/modeled/amplifyconfiguration'; import { Schema } from '../fixtures/modeled/schema'; @@ -53,7 +53,9 @@ function makeAppSyncStreams() { ); if (matchedType) { return new Observable(subscriber => { - streams[matchedType[1].toLowerCase()] = subscriber; + streams[ + matchedType[1].toLowerCase() as 'create' | 'update' | 'delete' + ] = subscriber; }); } }); @@ -103,15 +105,52 @@ const USER_AGENT_DETAILS = { }; describe('generateClient', () => { - // test('raises clear error when API GraphQL isnt configured', () => { - // const getConfig = jest.fn().mockReturnValue({}); - // const amplify = { - // getConfig, - // } as unknown as AmplifyClassV6; - // expect(() => generateClient({ amplify })).toThrow( - // 'The API configuration is missing. This is likely due to Amplify.configure() not being called prior to generateClient()' - // ); - // }); + describe('client `models` property', () => { + const expectedModelsProperties = [ + 'Todo', + 'Note', + 'TodoMetadata', + 'ThingWithCustomerOwnerField', + 'ThingWithOwnerFieldSpecifiedInModel', + 'ThingWithAPIKeyAuth', + 'ThingWithoutExplicitAuth', + 'ThingWithCustomPk', + 'CommunityPoll', + 'CommunityPollAnswer', + 'CommunityPollVote', + 'CommunityPost', + ]; + + it('generates `models` property when Amplify.getConfig() returns valid GraphQL provider config', () => { + Amplify.configure(configFixture); // clear the resource config + + const client = generateClient({ amplify: Amplify }); + + expect(Object.keys(client.models)).toEqual(expectedModelsProperties); + }); + + it('generates `models` property when Amplify.configure() is called later with a valid GraphQL provider config', async () => { + Amplify.configure({}); // clear the ResourceConfig mimic Amplify.configure has not been called + const client = generateClient({ amplify: Amplify }); + + expect(Object.keys(client.models)).toHaveLength(0); + + Amplify.configure(configFixture); + + expect(Object.keys(client.models)).toEqual(expectedModelsProperties); + }); + + it('generates `models` property throwing error when there is no valid GraphQL provider config can be resolved', () => { + Amplify.configure({}); // clear the ResourceConfig mimic Amplify.configure has not been called + const client = generateClient({ amplify: Amplify }); + + expect(() => { + client.models.Todo.create({ name: 'todo' }); + }).toThrow( + 'Could not generate client. This is likely due to Amplify.configure() not being called prior to generateClient().', + ); + }); + }); test('can produce a client bound to an arbitrary amplify object for getConfig()', async () => { // TS lies: We don't care what `amplify` is or does. We want want to make sure diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index 7bf63b93835..989f87a9280 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -10,6 +10,8 @@ import { __headers, } from '../types'; import { ClientGenerationParams } from './types'; +import { ModelTypes } from '@aws-amplify/data-schema-types'; +import { Hub, ResourcesConfig } from '@aws-amplify/core'; /** * @private @@ -34,7 +36,50 @@ export function generateClient = never>( models: {}, } as any; - client.models = generateModelsProperty(client, params); + const config = params.amplify.getConfig(); + + if (!config.API?.GraphQL) { + client.models = emptyModels as ModelTypes; + generateModelsPropertyOnAmplifyConfigure(client); + } else { + client.models = generateModelsProperty( + client, + config.API?.GraphQL, + ); + } return client as V6Client; } + +const generateModelsPropertyOnAmplifyConfigure = < + T extends Record = never, +>(clientRef: any) => { + Hub.listen('core', coreEvent => { + const { event, data } = coreEvent.payload; + + if (event !== 'configure') { + return; + } + + // data is guaranteed to be `ResourcesConfig` when the event is `configure` + const resourceConfig = data as ResourcesConfig; + + if (resourceConfig.API?.GraphQL) { + clientRef.models = generateModelsProperty( + clientRef, + resourceConfig.API?.GraphQL, + ); + } + }); +}; + +const emptyModels = new Proxy( + {}, + { + get() { + throw new Error( + 'Could not generate client. This is likely due to Amplify.configure() not being called prior to generateClient().', + ); + }, + }, +); diff --git a/packages/api-graphql/src/internals/generateModelsProperty.ts b/packages/api-graphql/src/internals/generateModelsProperty.ts index ec7a9e009cf..d8e82837931 100644 --- a/packages/api-graphql/src/internals/generateModelsProperty.ts +++ b/packages/api-graphql/src/internals/generateModelsProperty.ts @@ -10,27 +10,16 @@ import { getFactory } from './operations/get'; import { subscriptionFactory } from './operations/subscription'; import { observeQueryFactory } from './operations/observeQuery'; import { ModelIntrospectionSchema } from '@aws-amplify/core/internals/utils'; +import { GraphQLProviderConfig } from '@aws-amplify/core'; export function generateModelsProperty = never>( client: V6Client>, - params: ClientGenerationParams + graphqlConfig: GraphQLProviderConfig['GraphQL'], ): ModelTypes { const models = {} as any; - const config = params.amplify.getConfig(); - - if (!config.API?.GraphQL) { - // breaks compatibility with certain bundler, e.g. Vite where component files are evaluated before - // the entry point causing false positive errors. Revisit how to better handle this post-launch - - // throw new Error( - // 'The API configuration is missing. This is likely due to Amplify.configure() not being called - // prior to generateClient().' - // ); - return {} as ModelTypes; - } const modelIntrospection: ModelIntrospectionSchema | undefined = - config.API.GraphQL.modelIntrospection; + graphqlConfig.modelIntrospection; if (!modelIntrospection) { return {} as ModelTypes; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 05c753c7e6b..427b49e82f6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,6 +26,7 @@ export { AuthUserPoolConfig, AuthUserPoolAndIdentityPoolConfig, APIConfig, + GraphQLProviderConfig, PredictionsConfig, StorageAccessLevel, StorageConfig, diff --git a/packages/core/src/singleton/types.ts b/packages/core/src/singleton/types.ts index a3787dc4b75..00abfe261fd 100644 --- a/packages/core/src/singleton/types.ts +++ b/packages/core/src/singleton/types.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { APIConfig, LibraryAPIOptions } from './API/types'; +import { + APIConfig, + LibraryAPIOptions, + GraphQLProviderConfig, +} from './API/types'; import { AnalyticsConfig } from './Analytics/types'; import { AuthConfig, @@ -57,6 +61,7 @@ export { AuthIdentityPoolConfig, AuthUserPoolAndIdentityPoolConfig, GetCredentialsOptions, + GraphQLProviderConfig, InteractionsConfig, PredictionsConfig, StorageAccessLevel,