Skip to content

Commit ba19bb4

Browse files
authored
fix: ensure MIS model name mismatch cannot cause unexpected behavior (#403)
1 parent 89ad85a commit ba19bb4

File tree

8 files changed

+208
-34
lines changed

8 files changed

+208
-34
lines changed

.changeset/stale-apricots-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/data-schema': patch
3+
---
4+
5+
fix prevent mis model mismatch

packages/data-schema/__tests__/runtime/APIClient.test.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -809,27 +809,39 @@ describe('generateGraphQLDocument()', () => {
809809
};
810810

811811
test.each([
812-
['GET', 'User', '$userId: ID!'],
812+
['GET', userSchemaModel, '$userId: ID!'],
813813
[
814814
'GET',
815-
'Product',
815+
productSchemaModel,
816816
'$sku: String!,$factoryId: String!,$warehouseId: String!',
817817
],
818-
['GET', 'person', 'getPerson'],
819-
['LIST', 'person', 'listPeople'],
820-
['LIST', 'person', '$filter: ModelPersonFilterInput'],
821-
['CREATE', 'person', '$input: CreatePersonInput!'],
822-
['UPDATE', 'person', '$input: UpdatePersonInput!'],
823-
['DELETE', 'person', '$input: DeletePersonInput!'],
824-
['ONCREATE', 'person', '$filter: ModelSubscriptionPersonFilterInput'],
825-
['ONUPDATE', 'person', '$filter: ModelSubscriptionPersonFilterInput'],
826-
['ONDELETE', 'person', '$filter: ModelSubscriptionPersonFilterInput'],
818+
['GET', personSchemaModel, 'getPerson'],
819+
['LIST', personSchemaModel, 'listPeople'],
820+
['LIST', personSchemaModel, '$filter: ModelPersonFilterInput'],
821+
['CREATE', personSchemaModel, '$input: CreatePersonInput!'],
822+
['UPDATE', personSchemaModel, '$input: UpdatePersonInput!'],
823+
['DELETE', personSchemaModel, '$input: DeletePersonInput!'],
824+
[
825+
'ONCREATE',
826+
personSchemaModel,
827+
'$filter: ModelSubscriptionPersonFilterInput',
828+
],
829+
[
830+
'ONUPDATE',
831+
personSchemaModel,
832+
'$filter: ModelSubscriptionPersonFilterInput',
833+
],
834+
[
835+
'ONDELETE',
836+
personSchemaModel,
837+
'$filter: ModelSubscriptionPersonFilterInput',
838+
],
827839
])(
828840
'%s generates operation name or arguments for model %s to contain %s',
829-
(modelOperation, modelName, expectedArgs) => {
841+
(modelOperation, model, expectedArgs) => {
830842
const document = generateGraphQLDocument(
831843
mockModelDefinitions,
832-
modelName,
844+
model,
833845
modelOperation as ModelOperation,
834846
);
835847

@@ -873,31 +885,31 @@ describe('generateGraphQLDocument()', () => {
873885

874886
test.each([
875887
[
876-
'EnumAsIndexPartitionKey',
888+
modelIntroSchema.models.EnumAsIndexPartitionKey,
877889
'$status: EnumAsIndexPartitionKeyStatus!',
878890
getIndexMeta('EnumAsIndexPartitionKey'),
879891
{ status: 'yes' },
880892
],
881893
[
882-
'EnumAsIndexSortKey',
894+
modelIntroSchema.models.EnumAsIndexSortKey,
883895
'$status: ModelStringKeyConditionInput',
884896
getIndexMeta('EnumAsIndexSortKey'),
885897
{ title: 'test' },
886898
],
887899
[
888-
'RefEnumAsIndexPartitionKey',
900+
modelIntroSchema.models.RefEnumAsIndexPartitionKey,
889901
'$status: EnumIndexStatus!',
890902
getIndexMeta('RefEnumAsIndexPartitionKey'),
891903
{ status: 'yes' },
892904
],
893905
[
894-
'RefEnumAsIndexSortKey',
906+
modelIntroSchema.models.RefEnumAsIndexSortKey,
895907
'$status: ModelStringKeyConditionInput',
896908
getIndexMeta('RefEnumAsIndexSortKey'),
897909
{ title: 'test' },
898910
],
899911
[
900-
'SecondaryIndexModel',
912+
modelIntroSchema.models.SecondaryIndexModel,
901913
'$timestamp: ModelIntKeyConditionInput',
902914
getIndexMeta(
903915
'SecondaryIndexModel',
@@ -907,10 +919,10 @@ describe('generateGraphQLDocument()', () => {
907919
],
908920
])(
909921
'generates document for model %p containing input type: %p',
910-
(modelName, expectedSortKeyInputString, indexMeta, args) => {
922+
(model, expectedSortKeyInputString, indexMeta, args) => {
911923
const document = generateGraphQLDocument(
912924
modelIntroSchema,
913-
modelName,
925+
model,
914926
modelOperation,
915927
args,
916928
indexMeta,

packages/data-schema/src/runtime/internals/APIClient.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

33
// SPDX-License-Identifier: Apache-2.0
4-
54
import {
65
AmplifyServer,
76
AssociationBelongsTo,
@@ -245,7 +244,7 @@ export function initializeModel(
245244
if (record[curVal]) {
246245
acc[curVal] = record[curVal];
247246
}
248-
return acc;
247+
return acc;
249248
},
250249
{},
251250
);
@@ -843,13 +842,11 @@ export function generateSelectionSet(
843842

844843
export function generateGraphQLDocument(
845844
modelIntrospection: ModelIntrospectionSchema,
846-
modelName: string,
845+
modelDefinition: SchemaModel,
847846
modelOperation: ModelOperation,
848847
listArgs?: ListArgs | QueryArgs,
849848
indexMeta?: IndexMeta,
850849
): string {
851-
const modelDefinition = modelIntrospection.models[modelName];
852-
853850
const {
854851
name,
855852
pluralName,
@@ -917,7 +914,7 @@ export function generateGraphQLDocument(
917914
)?.properties?.name;
918915

919916
skQueryArgs = {
920-
[compositeSkArgName]: `Model${capitalize(modelName)}${capitalize(keyName)}CompositeKeyConditionInput`,
917+
[compositeSkArgName]: `Model${capitalize(name)}${capitalize(keyName)}CompositeKeyConditionInput`,
921918
};
922919
}
923920

@@ -941,7 +938,7 @@ export function generateGraphQLDocument(
941938

942939
const selectionSetFields = generateSelectionSet(
943940
modelIntrospection,
944-
modelName,
941+
name,
945942
selectionSet as ListArgs['selectionSet'],
946943
);
947944

@@ -1011,7 +1008,7 @@ export function generateGraphQLDocument(
10111008
const compositeSkArgName = resolvedSkName(sortKeyFieldNames);
10121009

10131010
return {
1014-
[compositeSkArgName]: `Model${capitalize(modelName)}PrimaryCompositeKeyConditionInput`,
1011+
[compositeSkArgName]: `Model${capitalize(name)}PrimaryCompositeKeyConditionInput`,
10151012
};
10161013
}
10171014
}

packages/data-schema/src/runtime/internals/operations/get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function _get(
9292

9393
const query = generateGraphQLDocument(
9494
modelIntrospection,
95-
name,
95+
model,
9696
operation,
9797
options,
9898
);

packages/data-schema/src/runtime/internals/operations/indexQuery.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function _indexQuery(
120120

121121
const query = generateGraphQLDocument(
122122
modelIntrospection,
123-
name,
123+
model,
124124
'INDEX_QUERY',
125125
args,
126126
indexMeta,
@@ -189,7 +189,12 @@ function _indexQuery(
189189
const { data, errors } = error;
190190

191191
// `data` is not `null`, and is not an empty object:
192-
if (data !== undefined && data !== null && Object.keys(data).length !== 0 && errors) {
192+
if (
193+
data !== undefined &&
194+
data !== null &&
195+
Object.keys(data).length !== 0 &&
196+
errors
197+
) {
193198
const [key] = Object.keys(data);
194199

195200
if (data[key]?.items) {

packages/data-schema/src/runtime/internals/operations/list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function listFactory(
6060
getInternals,
6161
args,
6262
undefined,
63-
customUserAgentDetails
63+
customUserAgentDetails,
6464
);
6565
};
6666

@@ -81,7 +81,7 @@ function _list(
8181

8282
const query = generateGraphQLDocument(
8383
modelIntrospection,
84-
name,
84+
model,
8585
'LIST',
8686
args,
8787
);

packages/data-schema/src/runtime/internals/operations/subscription.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function subscriptionFactory(
3232
const subscription = (args?: Record<string, any>) => {
3333
const query = generateGraphQLDocument(
3434
modelIntrospection,
35-
name,
35+
model,
3636
operation,
3737
args,
3838
);
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { a, ClientSchema } from '@aws-amplify/data-schema';
2+
import { Amplify } from 'aws-amplify';
3+
import {
4+
buildAmplifyConfig,
5+
mockedGenerateClient,
6+
optionsAndHeaders,
7+
expectGraphqlMatches,
8+
} from '../../utils';
9+
10+
const sampleTodo = {
11+
__typename: 'Todo',
12+
id: 'some-id',
13+
content: 'some content',
14+
description: 'something something',
15+
owner: 'some-body',
16+
done: false,
17+
updatedAt: '2024-03-01T19:05:44.536Z',
18+
createdAt: '2024-03-01T18:05:44.536Z',
19+
};
20+
21+
function manipulateMis(config: Awaited<ReturnType<typeof buildAmplifyConfig>>) {
22+
const todoDef = { ...config.modelIntrospection.models['Todo'] };
23+
const notTodoDef = { ...config.modelIntrospection.models['NotTodo'] };
24+
25+
// swap definitions
26+
config.modelIntrospection.models['Todo'] = notTodoDef;
27+
config.modelIntrospection.models['NotTodo'] = todoDef;
28+
29+
return config;
30+
}
31+
32+
describe('Manipulated MIS cannot be used to call the wrong model', () => {
33+
const schema = a
34+
.schema({
35+
Todo: a.model({
36+
content: a.string(),
37+
}),
38+
NotTodo: a.model({
39+
content: a.string(),
40+
}),
41+
})
42+
.authorization((allow) => [allow.publicApiKey()]);
43+
type Schema = ClientSchema<typeof schema>;
44+
45+
afterEach(() => {
46+
jest.clearAllMocks();
47+
});
48+
49+
test('create performs operation against intended model', async () => {
50+
// #region mocking
51+
const { spy, generateClient } = mockedGenerateClient([
52+
{
53+
data: {
54+
createTodo: sampleTodo,
55+
},
56+
},
57+
]);
58+
59+
// simulated amplifyconfiguration.json
60+
const original = await buildAmplifyConfig(schema);
61+
const config = manipulateMis(original);
62+
// #endregion mocking
63+
64+
// App.tsx
65+
Amplify.configure(config);
66+
const client = generateClient<Schema>();
67+
const { errors, data: newTodo } = await client.models.Todo.create({
68+
content: 'My new todo',
69+
});
70+
// #endregion
71+
72+
// #region assertions
73+
expectGraphqlMatches(
74+
optionsAndHeaders(spy)[0][0].query,
75+
`mutation($input: CreateTodoInput!) {
76+
createTodo(input: $input) { id content createdAt updatedAt }
77+
}`,
78+
);
79+
expect(errors).toBeUndefined();
80+
expect(newTodo).toEqual(sampleTodo);
81+
// #endregion assertions
82+
});
83+
84+
test('update performs operation against intended model', async () => {
85+
// #region mocking
86+
const { spy, generateClient } = mockedGenerateClient([
87+
{
88+
data: {
89+
updateTodo: sampleTodo,
90+
},
91+
},
92+
]);
93+
94+
// simulated amplifyconfiguration.json
95+
const original = await buildAmplifyConfig(schema);
96+
const config = manipulateMis(original);
97+
// #endregion mocking
98+
99+
Amplify.configure(config);
100+
const client = generateClient<Schema>();
101+
const { data: updatedTodo, errors } = await client.models.Todo.update({
102+
id: 'some_id',
103+
content: 'Updated content',
104+
});
105+
// #endregion
106+
107+
// #region assertions
108+
expectGraphqlMatches(
109+
optionsAndHeaders(spy)[0][0].query,
110+
`mutation($input: UpdateTodoInput!) {
111+
updateTodo(input: $input) { id content createdAt updatedAt }
112+
}`,
113+
);
114+
expect(errors).toBeUndefined();
115+
expect(updatedTodo).toEqual(sampleTodo);
116+
// #endregion assertions
117+
});
118+
119+
test('delete performs operation against intended model', async () => {
120+
// #region mocking
121+
const { spy, generateClient } = mockedGenerateClient([
122+
{
123+
data: {
124+
deleteTodo: sampleTodo,
125+
},
126+
},
127+
]);
128+
129+
// simulated amplifyconfiguration.json
130+
const original = await buildAmplifyConfig(schema);
131+
const config = manipulateMis(original);
132+
// #endregion mocking
133+
134+
// #region
135+
Amplify.configure(config);
136+
const client = generateClient<Schema>();
137+
const toBeDeletedTodo = {
138+
id: '123123213',
139+
};
140+
const { data: deletedTodo, errors } =
141+
await client.models.Todo.delete(toBeDeletedTodo);
142+
// #endregion
143+
144+
// #region assertions
145+
expectGraphqlMatches(
146+
optionsAndHeaders(spy)[0][0].query,
147+
`mutation($input: DeleteTodoInput!) {
148+
deleteTodo(input: $input) { id content createdAt updatedAt }
149+
}`,
150+
);
151+
expect(errors).toBeUndefined();
152+
expect(deletedTodo).toEqual(sampleTodo);
153+
// #endregion assertions
154+
});
155+
});

0 commit comments

Comments
 (0)