From 22e249d361492d2d1803d6c8322efa7cc940d3bc Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Wed, 7 Feb 2024 14:45:55 -0800 Subject: [PATCH] feat(api-graphql): add .enums property to the GQL client --- .../internals/generateClient.test.ts | 38 +++++++++++- .../utils/generateEnumsProperty.test.ts | 46 +++++++++++++++ .../src/internals/generateClient.ts | 58 ++++++++----------- .../internals/utils/generateEnumsProperty.ts | 30 ++++++++++ .../{ => utils}/generateModelsProperty.ts | 31 +++++----- .../utils/isApiGraphQLProviderConfig.ts | 10 ++++ .../isConfigureEventWithResourceConfig.ts | 13 +++++ packages/api-graphql/src/types/index.ts | 3 +- packages/api-graphql/tsconfig.json | 6 +- 9 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 packages/api-graphql/__tests__/internals/utils/generateEnumsProperty.test.ts create mode 100644 packages/api-graphql/src/internals/utils/generateEnumsProperty.ts rename packages/api-graphql/src/internals/{ => utils}/generateModelsProperty.ts (67%) create mode 100644 packages/api-graphql/src/internals/utils/isApiGraphQLProviderConfig.ts create mode 100644 packages/api-graphql/src/internals/utils/isConfigureEventWithResourceConfig.ts diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 9ff372ce994..f4b4bdab549 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, Hub } from '@aws-amplify/core'; +import { Amplify, AmplifyClassV6 } from '@aws-amplify/core'; import { generateClient } from '../../src/internals'; import configFixture from '../fixtures/modeled/amplifyconfiguration'; import { Schema } from '../fixtures/modeled/schema'; @@ -147,7 +147,41 @@ describe('generateClient', () => { 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().', + 'Client is not generate. This is likely due to `Amplify.configure()` not being called prior to `generateClient()` or because the configuration passed to `Amplify.configure()` is missing GraphQL provider configuration.', + ); + }); + }); + + describe('client `enums` property', () => { + const expectedEnumsProperties = ['Status']; + + it('generates `enums` 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.enums)).toEqual(expectedEnumsProperties); + }); + + it('generates `enums` 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.enums)).toHaveLength(0); + + Amplify.configure(configFixture); + + expect(Object.keys(client.enums)).toEqual(expectedEnumsProperties); + }); + + 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.enums.Status.values() + }).toThrow( + 'Client is not generate. This is likely due to `Amplify.configure()` not being called prior to `generateClient()` or because the configuration passed to `Amplify.configure()` is missing GraphQL provider configuration.', ); }); }); diff --git a/packages/api-graphql/__tests__/internals/utils/generateEnumsProperty.test.ts b/packages/api-graphql/__tests__/internals/utils/generateEnumsProperty.test.ts new file mode 100644 index 00000000000..30bd90a4008 --- /dev/null +++ b/packages/api-graphql/__tests__/internals/utils/generateEnumsProperty.test.ts @@ -0,0 +1,46 @@ +import { GraphQLProviderConfig } from '@aws-amplify/core/internals/utils'; +import { generateEnumsProperty } from '../../../src/internals/utils/generateEnumsProperty'; + +describe('generateEnumsProperty()', () => { + it('returns an empty object when there is no valid `modelIntrospection`', () => { + const mockAPIGraphQLConfig: GraphQLProviderConfig['GraphQL'] = { + endpoint: 'endpoint', + defaultAuthMode: 'iam', + }; + const result = generateEnumsProperty(mockAPIGraphQLConfig); + + expect(Object.keys(result)).toHaveLength(0); + }); + + it('returns expected `enums` object', () => { + const mockAPIGraphQLConfig: GraphQLProviderConfig['GraphQL'] = { + endpoint: 'endpoint', + defaultAuthMode: 'iam', + modelIntrospection: { + version: 1, + models: {}, + nonModels: {}, + enums: { + TodoStatus: { + name: 'TodoStatus', + values: ['Planned', 'InProgress', 'Completed'], + }, + SomeEnum: { + name: 'SomeEnum', + values: ['value1', 'value2'], + }, + }, + }, + }; + + const result = generateEnumsProperty(mockAPIGraphQLConfig); + + expect(Object.keys(result)).toEqual(['TodoStatus', 'SomeEnum']); + expect((result as any).TodoStatus.values()).toEqual([ + 'Planned', + 'InProgress', + 'Completed', + ]); + expect((result as any).SomeEnum.values()).toEqual(['value1', 'value2']); + }); +}); diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index e3cd5e95f01..9c6fc0297d3 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { Hub } from '@aws-amplify/core'; +import { EnumTypes, ModelTypes } from '@aws-amplify/data-schema-types'; import { graphql, cancel, isCancelError } from './v6'; -import { generateModelsProperty } from './generateModelsProperty'; +import { generateEnumsProperty } from './utils/generateEnumsProperty'; +import { generateModelsProperty } from './utils/generateModelsProperty'; +import { isApiGraphQLConfig } from './utils/isApiGraphQLProviderConfig'; import { V6Client, __amplify, @@ -10,8 +14,7 @@ import { __headers, } from '../types'; import { ClientGenerationParams } from './types'; -import { ModelTypes } from '@aws-amplify/data-schema-types'; -import { Hub, HubCapsule, ResourcesConfig } from '@aws-amplify/core'; +import { isConfigureEventWithResourceConfig } from './utils/isConfigureEventWithResourceConfig'; /** * @private @@ -23,7 +26,7 @@ import { Hub, HubCapsule, ResourcesConfig } from '@aws-amplify/core'; * @returns */ export function generateClient = never>( - params: ClientGenerationParams + params: ClientGenerationParams, ): V6Client { const client = { [__amplify]: params.amplify, @@ -33,12 +36,16 @@ export function generateClient = never>( graphql, cancel, isCancelError, - models: {}, + models: emptyProperty as ModelTypes, + enums: emptyProperty as EnumTypes, } as any; - const config = params.amplify.getConfig(); + const apiGraphqlConfig = params.amplify.getConfig().API?.GraphQL; - if (!config.API?.GraphQL) { + if (isApiGraphQLConfig(apiGraphqlConfig)) { + client.models = generateModelsProperty(client, apiGraphqlConfig); + client.enums = generateEnumsProperty(apiGraphqlConfig); + } else { // This happens when the `Amplify.configure()` call gets evaluated after the `generateClient()` call. // // Cause: when the `generateClient()` and the `Amplify.configure()` calls are located in @@ -51,52 +58,33 @@ export function generateClient = never>( // // TODO: revisit, and reverify this approach when enabling multiple clients for multi-endpoints // configuration. - client.models = emptyModels as ModelTypes; - generateModelsPropertyOnAmplifyConfigure(client); - } else { - client.models = generateModelsProperty( - client, - config.API?.GraphQL, - ); + generateModelsPropertyOnAmplifyConfigure(client); } return client as V6Client; } -const generateModelsPropertyOnAmplifyConfigure = < - T extends Record = never, ->(clientRef: any) => { +const generateModelsPropertyOnAmplifyConfigure = (clientRef: any) => { Hub.listen('core', coreEvent => { - if (!isConfigureEvent(coreEvent.payload)) { + if (!isConfigureEventWithResourceConfig(coreEvent.payload)) { return; } - const { data: resourceConfig } = coreEvent.payload; + const apiGraphQLConfig = coreEvent.payload.data.API?.GraphQL; - if (resourceConfig.API?.GraphQL) { - clientRef.models = generateModelsProperty( - clientRef, - resourceConfig.API?.GraphQL, - ); + if (isApiGraphQLConfig(apiGraphQLConfig)) { + clientRef.models = generateModelsProperty(clientRef, apiGraphQLConfig); + clientRef.enums = generateEnumsProperty(apiGraphQLConfig); } }); }; -function isConfigureEvent( - payload: HubCapsule<'core', { event: string; data?: unknown }>['payload'], -): payload is { - event: 'configure'; - data: ResourcesConfig; -} { - return payload.event === 'configure'; -} - -const emptyModels = new Proxy( +const emptyProperty = new Proxy( {}, { get() { throw new Error( - 'Could not generate client. This is likely due to Amplify.configure() not being called prior to generateClient().', + 'Client is not generate. This is likely due to `Amplify.configure()` not being called prior to `generateClient()` or because the configuration passed to `Amplify.configure()` is missing GraphQL provider configuration.', ); }, }, diff --git a/packages/api-graphql/src/internals/utils/generateEnumsProperty.ts b/packages/api-graphql/src/internals/utils/generateEnumsProperty.ts new file mode 100644 index 00000000000..80840941e6e --- /dev/null +++ b/packages/api-graphql/src/internals/utils/generateEnumsProperty.ts @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ModelIntrospectionSchema, GraphQLProviderConfig } from '@aws-amplify/core/internals/utils'; +import { EnumTypes } from '@aws-amplify/data-schema-types'; + +export const generateEnumsProperty = = never>( + graphqlConfig: GraphQLProviderConfig['GraphQL'], +): EnumTypes => { + const modelIntrospection: ModelIntrospectionSchema | undefined = + graphqlConfig.modelIntrospection; + + if (!modelIntrospection) { + return {} as EnumTypes; + } + + const enums: { + [EnumName in keyof typeof modelIntrospection.enums]: { + values: () => string[]; + }; + } = {}; + + for (const [_, enumData] of Object.entries(modelIntrospection.enums)) { + enums[enumData.name] = { + values: () => enumData.values, + }; + } + + return enums as EnumTypes; +}; diff --git a/packages/api-graphql/src/internals/generateModelsProperty.ts b/packages/api-graphql/src/internals/utils/generateModelsProperty.ts similarity index 67% rename from packages/api-graphql/src/internals/generateModelsProperty.ts rename to packages/api-graphql/src/internals/utils/generateModelsProperty.ts index d8e82837931..5f75433dee7 100644 --- a/packages/api-graphql/src/internals/generateModelsProperty.ts +++ b/packages/api-graphql/src/internals/utils/generateModelsProperty.ts @@ -1,25 +1,26 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ModelTypes } from '@aws-amplify/data-schema-types'; -import { graphQLOperationsInfo, ModelOperation } from './APIClient'; -import { ClientGenerationParams } from './types'; -import { V6Client, __authMode, __authToken } from '../types'; +import { graphQLOperationsInfo, ModelOperation } from '../APIClient'; +import { V6Client, __authMode, __authToken } from '../../types'; -import { listFactory } from './operations/list'; -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'; +import { listFactory } from '../operations/list'; +import { getFactory } from '../operations/get'; +import { subscriptionFactory } from '../operations/subscription'; +import { observeQueryFactory } from '../operations/observeQuery'; +import { + GraphQLProviderConfig, + ModelIntrospectionSchema, +} from '@aws-amplify/core/internals/utils'; export function generateModelsProperty = never>( client: V6Client>, - graphqlConfig: GraphQLProviderConfig['GraphQL'], + apiGraphQLConfig: GraphQLProviderConfig['GraphQL'], ): ModelTypes { const models = {} as any; const modelIntrospection: ModelIntrospectionSchema | undefined = - graphqlConfig.modelIntrospection; + apiGraphQLConfig.modelIntrospection; if (!modelIntrospection) { return {} as ModelTypes; @@ -40,14 +41,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); @@ -56,10 +57,10 @@ export function generateModelsProperty = never>( client, modelIntrospection, model, - operation + operation, ); } - } + }, ); } diff --git a/packages/api-graphql/src/internals/utils/isApiGraphQLProviderConfig.ts b/packages/api-graphql/src/internals/utils/isApiGraphQLProviderConfig.ts new file mode 100644 index 00000000000..06691939833 --- /dev/null +++ b/packages/api-graphql/src/internals/utils/isApiGraphQLProviderConfig.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { GraphQLProviderConfig } from '@aws-amplify/core/internals/utils'; + +export function isApiGraphQLConfig( + apiGraphQLConfig: GraphQLProviderConfig['GraphQL'] | undefined, +): apiGraphQLConfig is GraphQLProviderConfig['GraphQL'] { + return apiGraphQLConfig !== undefined; +} diff --git a/packages/api-graphql/src/internals/utils/isConfigureEventWithResourceConfig.ts b/packages/api-graphql/src/internals/utils/isConfigureEventWithResourceConfig.ts new file mode 100644 index 00000000000..de5257847b6 --- /dev/null +++ b/packages/api-graphql/src/internals/utils/isConfigureEventWithResourceConfig.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HubCapsule, ResourcesConfig } from '@aws-amplify/core'; + +export function isConfigureEventWithResourceConfig( + payload: HubCapsule<'core', { event: string; data?: unknown }>['payload'], +): payload is { + event: 'configure'; + data: ResourcesConfig; +} { + return payload.event === 'configure'; +} diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 5b643086511..9b920691e1c 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AmplifyClassV6, ResourcesConfig } from '@aws-amplify/core'; -import { ModelTypes, CustomHeaders } from '@aws-amplify/data-schema-types'; +import { ModelTypes, CustomHeaders, EnumTypes } from '@aws-amplify/data-schema-types'; import { Source, DocumentNode, GraphQLError } from 'graphql'; export { OperationTypeNode } from 'graphql'; import { Observable } from 'rxjs'; @@ -382,6 +382,7 @@ export type V6Client = never> = ExcludeNeverFields<{ cancel: (promise: Promise, message?: string) => boolean; isCancelError: (error: any) => boolean; models: ModelTypes; + enums: EnumTypes; }>; export type V6ClientSSRRequest = never> = diff --git a/packages/api-graphql/tsconfig.json b/packages/api-graphql/tsconfig.json index 838b818549e..fa1de5d74f5 100644 --- a/packages/api-graphql/tsconfig.json +++ b/packages/api-graphql/tsconfig.json @@ -1,7 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "strictNullChecks": true + "strictNullChecks": true, + "baseUrl": ".", + "paths": { + "@aws-amplify/data-schema-types": ["../../node_modules/@aws-amplify/data-schema-types"] + } }, "include": ["./src", "__tests__"] }