diff --git a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts index 77c21ad8757..ab3ce689644 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts @@ -1278,12 +1278,18 @@ const amplifyConfig = { isRequired: true, attributes: [], }, - warehouseId: { - name: 'warehouseId', + warehouse: { + name: 'warehouse', isArray: false, - type: 'String', - isRequired: true, + type: { + model: 'Warehouse', + }, + isRequired: false, attributes: [], + association: { + connectionType: 'BELONGS_TO', + targetNames: ['warehouseProductsId'], + }, }, description: { name: 'description', @@ -1301,10 +1307,10 @@ const amplifyConfig = { isRequired: false, attributes: [], }, - owner: { - name: 'owner', + warehouseProductsId: { + name: 'warehouseProductsId', isArray: false, - type: 'String', + type: 'ID', isRequired: false, attributes: [], }, @@ -1312,15 +1318,17 @@ const amplifyConfig = { name: 'createdAt', isArray: false, type: 'AWSDateTime', - isRequired: true, + isRequired: false, attributes: [], + isReadOnly: true, }, updatedAt: { name: 'updatedAt', isArray: false, type: 'AWSDateTime', - isRequired: true, + isRequired: false, attributes: [], + isReadOnly: true, }, }, syncable: true, @@ -1333,7 +1341,7 @@ const amplifyConfig = { { type: 'key', properties: { - fields: ['sku', 'factoryId', 'warehouseId'], + fields: ['sku', 'factoryId'], }, }, { @@ -1358,7 +1366,87 @@ const amplifyConfig = { primaryKeyInfo: { isCustomPrimaryKey: true, primaryKeyFieldName: 'sku', - sortKeyFieldNames: ['factoryId', 'warehouseId'], + sortKeyFieldNames: ['factoryId'], + }, + }, + Warehouse: { + name: 'Warehouse', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + name: { + name: 'name', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + products: { + name: 'products', + isArray: true, + type: { + model: 'Product', + }, + isRequired: false, + attributes: [], + isArrayNullable: true, + association: { + connectionType: 'HAS_MANY', + associatedWith: ['warehouseProductsId'], + }, + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'Warehouses', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], + }, + { + allow: 'public', + operations: ['read'], + }, + ], + }, + }, + ], + primaryKeyInfo: { + isCustomPrimaryKey: false, + primaryKeyFieldName: 'id', + sortKeyFieldNames: [], }, }, ImplicitOwner: { diff --git a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts index 8a980ff9785..e2bf3c7b281 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts @@ -91,15 +91,19 @@ const schema = a.schema({ .model({ sku: a.string().required(), factoryId: a.string().required(), - warehouseId: a.string().required(), description: a.string(), + warehouse: a.belongsTo("Warehouse"), trackingMeta: a.customType({ productMeta: a.ref('ProductMeta'), note: a.string(), }), }) - .identifier(['sku', 'factoryId', 'warehouseId']) - .authorization([a.allow.public()]), + .identifier(['sku', 'factoryId']) + .authorization([a.allow.owner(), a.allow.public().to(["read"])]), + Warehouse: a.model({ + name: a.string().required(), + products: a.hasMany("Product"), + }).authorization([a.allow.owner(), a.allow.public().to(["read"])]), ProductMeta: a.customType({ releaseDate: a.date(), status: a.enum(['in_production', 'discontinued']), diff --git a/packages/api-graphql/__tests__/internals/APIClient.test.ts b/packages/api-graphql/__tests__/internals/APIClient.test.ts index 4004fc6cb50..ccf607de57f 100644 --- a/packages/api-graphql/__tests__/internals/APIClient.test.ts +++ b/packages/api-graphql/__tests__/internals/APIClient.test.ts @@ -415,6 +415,42 @@ describe('flattenItems', () => { expect(selSet).toEqual(expected); }); + + it('generates expected default selection set for nested model and custom type', () => { + const set = customSelectionSetToIR(modelIntroSchema, 'Warehouse', [ + 'id', + 'name', + 'products.*' + ]); + + const expected = { + id: '', + name: '', + products: { + items: { + createdAt: '', + updatedAt: '', + warehouseProductsId: '', + description: '', + factoryId: '', + owner: '', + sku: '', + trackingMeta: { + note: '', + productMeta: { + deepMeta: { + content: '', + }, + releaseDate: '', + status: '', + } + } + } + } + } + + expect(set).toEqual(expected); + }); }); describe('generateSelectionSet', () => { @@ -431,7 +467,7 @@ describe('flattenItems', () => { const generated = generateSelectionSet(modelIntroSchema, 'Product'); const expected = - 'sku factoryId warehouseId description trackingMeta { productMeta { releaseDate status deepMeta { content } } note } owner createdAt updatedAt'; + 'sku factoryId description trackingMeta { productMeta { releaseDate status deepMeta { content } } note } warehouseProductsId createdAt updatedAt owner'; expect(generated).toEqual(expected); }); diff --git a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap index 2f24c9d4731..88afaa71b88 100644 --- a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap @@ -4786,7 +4786,6 @@ exports[`generateClient custom operations can query with returnType of a model t echoModelHasNestedTypes(argumentContent: $argumentContent) { sku factoryId - warehouseId description trackingMeta { productMeta { @@ -4798,9 +4797,10 @@ exports[`generateClient custom operations can query with returnType of a model t } note } - owner + warehouseProductsId createdAt updatedAt + owner } } ", diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index ead08f04e17..7f547380abb 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -45,6 +45,7 @@ describe('generateClient', () => { 'Post', 'Comment', 'Product', + 'Warehouse', 'ImplicitOwner', 'CustomImplicitOwner', 'ModelGroupDefinedIn', @@ -5330,7 +5331,6 @@ describe('generateClient', () => { const mockReturnData = { sku: 'sku', factoryId: 'factoryId', - warehouseId: 'warehouseId', description: 'description', trackingMeta: { productMeta: { @@ -5357,7 +5357,10 @@ describe('generateClient', () => { }); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); - expect(result?.data).toEqual(mockReturnData); + expect(result?.data).toEqual({ + ...mockReturnData, + warehouse: expect.any(Function), + }); }); test('can query with returnType of string', async () => { diff --git a/packages/api-graphql/src/internals/APIClient.ts b/packages/api-graphql/src/internals/APIClient.ts index 4609703f894..39978196139 100644 --- a/packages/api-graphql/src/internals/APIClient.ts +++ b/packages/api-graphql/src/internals/APIClient.ts @@ -318,10 +318,10 @@ export type ModelOperation = keyof typeof graphQLOperationsInfo; const SELECTION_SET_WILDCARD = '*'; -export function defaultSelectionSetForNonModelWithIR( +export const getDefaultSelectionSetForNonModelWithIR = ( nonModelDefinition: SchemaNonModel, modelIntrospection: ModelIntrospectionSchema, -): Record { +): Record => { const { fields } = nonModelDefinition; const mappedFields = Object.values(fields) .map(({ type, name }) => { @@ -332,7 +332,7 @@ export function defaultSelectionSetForNonModelWithIR( if (typeof (type as NonModelFieldType).nonModel === 'string') { return [ name, - defaultSelectionSetForNonModelWithIR( + getDefaultSelectionSetForNonModelWithIR( modelIntrospection.nonModels[(type as NonModelFieldType).nonModel], modelIntrospection, ), @@ -352,7 +352,47 @@ export function defaultSelectionSetForNonModelWithIR( ); return Object.fromEntries(mappedFields); -} +}; + +const getDefaultSelectionSetForModelWithIR = ( + modelDefinition: SchemaModel, + modelIntrospection: ModelIntrospectionSchema, +): Record => { + const { fields } = modelDefinition; + const mappedFields = Object.values(fields) + .map(({ type, name }) => { + if ( + typeof (type as { enum: string }).enum === 'string' || + typeof type === 'string' + ) { + return [name, FIELD_IR]; + } + + if (typeof (type as NonModelFieldType).nonModel === 'string') { + return [ + name, + getDefaultSelectionSetForNonModelWithIR( + modelIntrospection.nonModels[(type as NonModelFieldType).nonModel], + modelIntrospection, + ), + ]; + } + + return undefined; + }) + .filter( + ( + pair: (string | Record)[] | undefined, + ): pair is (string | Record)[] => pair !== undefined, + ); + + const ownerFields = resolveOwnerFields(modelDefinition).map(field => [ + field, + FIELD_IR, + ]); + + return Object.fromEntries(mappedFields.concat(ownerFields)); +}; function defaultSelectionSetForModel(modelDefinition: SchemaModel): string[] { // fields that are explicitly part of the graphql schema; not @@ -445,7 +485,7 @@ export function customSelectionSetToIR( if (nested === SELECTION_SET_WILDCARD) { result = { - [fieldName]: defaultSelectionSetForNonModelWithIR( + [fieldName]: getDefaultSelectionSetForNonModelWithIR( relatedNonModelDefinition, modelIntrospection, ), @@ -471,8 +511,9 @@ export function customSelectionSetToIR( modelIntrospection.models[relatedModel]; result = { - [fieldName]: modelsDefaultSelectionSetIR( + [fieldName]: getDefaultSelectionSetForModelWithIR( nestedRelatedModelDefinition, + modelIntrospection, ), }; } else { @@ -529,23 +570,6 @@ export function customSelectionSetToIR( ); } -const modelsDefaultSelectionSetIR = (relatedModelDefinition: SchemaModel) => { - const defaultSelectionSet = defaultSelectionSetForModel( - relatedModelDefinition, - ); - - const reduced = defaultSelectionSet.reduce( - (acc: Record, curVal) => { - acc[curVal] = FIELD_IR; - - return acc; - }, - {}, - ); - - return reduced; -}; - /** * Stringifies selection set IR * * @example diff --git a/packages/api-graphql/src/internals/operations/custom.ts b/packages/api-graphql/src/internals/operations/custom.ts index a5c4ac8b69e..619cbefa7be 100644 --- a/packages/api-graphql/src/internals/operations/custom.ts +++ b/packages/api-graphql/src/internals/operations/custom.ts @@ -9,10 +9,10 @@ import { map } from 'rxjs'; import { authModeParams, - defaultSelectionSetForNonModelWithIR, flattenItems, generateSelectionSet, getCustomHeaders, + getDefaultSelectionSetForNonModelWithIR, initializeModel, selectionSetIRToString, } from '../APIClient'; @@ -290,7 +290,7 @@ function operationSelectionSet( const nonModel = modelIntrospection.nonModels[operation.type.nonModel]; return `{${selectionSetIRToString( - defaultSelectionSetForNonModelWithIR(nonModel, modelIntrospection), + getDefaultSelectionSetForNonModelWithIR(nonModel, modelIntrospection), )}}`; } else if (hasStringField(operation.type, 'model')) { return `{${generateSelectionSet(modelIntrospection, operation.type.model)}}`;