Skip to content

Commit a307a3c

Browse files
committed
feat: check for the presence of indices in checkCompatibility()
1 parent a35d203 commit a307a3c

File tree

5 files changed

+373
-1
lines changed

5 files changed

+373
-1
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import gql from 'graphql-tag';
2+
import {
3+
expectSingleCompatibilityIssue,
4+
expectToBeValid,
5+
} from '../implementation/validation-utils';
6+
import { runCheck } from './utils';
7+
8+
describe('checkModel', () => {
9+
describe('indices', () => {
10+
it('accepts if there are no indices', () => {
11+
const result = runCheck(
12+
gql`
13+
type Test @rootEntity @modules(in: "module1") {
14+
field: String @modules(all: true)
15+
}
16+
`,
17+
gql`
18+
type Test @rootEntity {
19+
field: String
20+
}
21+
`,
22+
);
23+
expectToBeValid(result);
24+
});
25+
26+
it('accepts if there are indices but there do not need to be any', () => {
27+
const result = runCheck(
28+
gql`
29+
type Test @rootEntity @modules(in: "module1") {
30+
field: String @modules(all: true)
31+
}
32+
`,
33+
gql`
34+
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
35+
field: String
36+
}
37+
`,
38+
);
39+
expectToBeValid(result);
40+
});
41+
42+
it('accepts if an index is required and present', () => {
43+
const result = runCheck(
44+
gql`
45+
type Test
46+
@rootEntity(indices: [{ fields: ["field"] }])
47+
@modules(in: "module1") {
48+
field: String @modules(all: true)
49+
}
50+
`,
51+
gql`
52+
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
53+
field: String
54+
}
55+
`,
56+
);
57+
expectToBeValid(result);
58+
});
59+
60+
it('accepts if an index is required and present with unique and sparse explicitly set to their default values', () => {
61+
const result = runCheck(
62+
gql`
63+
type Test
64+
@rootEntity(indices: [{ fields: ["field"] }])
65+
@modules(in: "module1") {
66+
field: String @modules(all: true)
67+
}
68+
`,
69+
gql`
70+
type Test
71+
@rootEntity(
72+
indices: [{ fields: ["field"], unique: false, sparse: false }]
73+
) {
74+
field: String
75+
}
76+
`,
77+
);
78+
expectToBeValid(result);
79+
});
80+
81+
it('rejects if an index is required and no index is present', () => {
82+
const result = runCheck(
83+
gql`
84+
type Test
85+
@rootEntity(indices: [{ fields: ["field"] }])
86+
@modules(in: "module1") {
87+
field: String @modules(all: true)
88+
}
89+
`,
90+
gql`
91+
type Test @rootEntity {
92+
field: String
93+
}
94+
`,
95+
);
96+
expectSingleCompatibilityIssue(
97+
result,
98+
'The following index is missing: {fields: ["field"]}',
99+
);
100+
});
101+
102+
it('rejects if an index is required, but it only exists with a different field', () => {
103+
const result = runCheck(
104+
gql`
105+
type Test
106+
@rootEntity(indices: [{ fields: ["field"], unique: true }])
107+
@modules(in: "module1") {
108+
field: String @modules(all: true)
109+
}
110+
`,
111+
gql`
112+
type Test @rootEntity(indices: [{ fields: ["field2"] }]) {
113+
field: String
114+
field2: String
115+
}
116+
`,
117+
);
118+
expectSingleCompatibilityIssue(
119+
result,
120+
'The following index is missing: {fields: ["field"], unique: true}',
121+
);
122+
});
123+
124+
it('rejects if an index is required, but it only exists with an additional field', () => {
125+
const result = runCheck(
126+
gql`
127+
type Test
128+
@rootEntity(indices: [{ fields: ["field"], unique: true }])
129+
@modules(in: "module1") {
130+
field: String @modules(all: true)
131+
}
132+
`,
133+
gql`
134+
type Test @rootEntity(indices: [{ fields: ["field", "field2"] }]) {
135+
field: String
136+
field2: String
137+
}
138+
`,
139+
);
140+
expectSingleCompatibilityIssue(
141+
result,
142+
'The following index is missing: {fields: ["field"], unique: true}',
143+
);
144+
});
145+
146+
it('rejects if an index is required, but it only exists with a different unique value', () => {
147+
const result = runCheck(
148+
gql`
149+
type Test
150+
@rootEntity(indices: [{ fields: ["field"], unique: true }])
151+
@modules(in: "module1") {
152+
field: String @modules(all: true)
153+
}
154+
`,
155+
gql`
156+
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
157+
field: String
158+
}
159+
`,
160+
);
161+
expectSingleCompatibilityIssue(
162+
result,
163+
'The following index is missing: {fields: ["field"], unique: true}',
164+
);
165+
});
166+
167+
it('rejects if an index is required, but it only exists with a different sparse value', () => {
168+
const result = runCheck(
169+
gql`
170+
type Test
171+
@rootEntity(indices: [{ fields: ["field"], unique: true }])
172+
@modules(in: "module1") {
173+
field: String @modules(all: true)
174+
}
175+
`,
176+
gql`
177+
type Test
178+
@rootEntity(indices: [{ fields: ["field"], unique: true, sparse: false }]) {
179+
field: String
180+
}
181+
`,
182+
);
183+
expectSingleCompatibilityIssue(
184+
result,
185+
'The following index is missing: {fields: ["field"], unique: true}',
186+
);
187+
});
188+
});
189+
190+
it('accepts if multiple indices are required and present', () => {
191+
const result = runCheck(
192+
gql`
193+
type Test
194+
@rootEntity(
195+
indices: [
196+
{ fields: ["field1"] }
197+
{ fields: ["field2", "field3"], unique: true }
198+
{ fields: ["field3"], sparse: true }
199+
]
200+
)
201+
@modules(in: "module1") {
202+
field1: String @modules(all: true)
203+
field2: String @modules(all: true)
204+
field3: String @modules(all: true)
205+
}
206+
`,
207+
gql`
208+
type Test
209+
@rootEntity(
210+
indices: [
211+
{ fields: ["field1"] }
212+
{ fields: ["field2", "field3"], unique: true }
213+
{ fields: ["field3"], sparse: true }
214+
]
215+
) {
216+
field1: String
217+
field2: String
218+
field3: String
219+
}
220+
`,
221+
);
222+
expectToBeValid(result);
223+
});
224+
225+
it('rejects if multiple indices are required but not all are present', () => {
226+
const result = runCheck(
227+
gql`
228+
type Test
229+
@rootEntity(
230+
indices: [
231+
{ fields: ["field1"] }
232+
{ fields: ["field2", "field3"], unique: true }
233+
{ fields: ["field3"], sparse: true }
234+
]
235+
)
236+
@modules(in: "module1") {
237+
field1: String @modules(all: true)
238+
field2: String @modules(all: true)
239+
field3: String @modules(all: true)
240+
}
241+
`,
242+
gql`
243+
type Test @rootEntity(indices: [{ fields: ["field1"] }]) {
244+
field1: String
245+
field2: String
246+
field3: String
247+
}
248+
`,
249+
);
250+
expectSingleCompatibilityIssue(
251+
result,
252+
'The following indices are missing: {fields: ["field2", "field3"], unique: true}, {fields: ["field3"], sparse: true}',
253+
);
254+
});
255+
256+
it('rejects if a field should be @unique but is not', () => {
257+
const result = runCheck(
258+
gql`
259+
type Test @rootEntity @modules(in: "module1", includeAllFields: true) {
260+
field: String @unique
261+
}
262+
`,
263+
gql`
264+
type Test @rootEntity {
265+
field: String
266+
}
267+
`,
268+
);
269+
expectSingleCompatibilityIssue(
270+
result,
271+
'The following index is missing: {fields: ["field"], unique: true}',
272+
);
273+
});
274+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { RootEntityType } from '../implementation';
2+
import { ValidationContext, ValidationMessage } from '../validation';
3+
import { IndexDefinitionConfig } from '../config';
4+
import { Kind, ObjectFieldNode, ObjectValueNode, print, StringValueNode } from 'graphql';
5+
import { INDICES_ARG } from '../../schema/constants';
6+
7+
export function checkIndices(
8+
typeToCheck: RootEntityType,
9+
baselineType: RootEntityType,
10+
context: ValidationContext,
11+
) {
12+
// use index config instead of indices because there is a transformation step that changes
13+
// indices and also adds new indices. It would be confusing report issues for these.
14+
const existing = new Set(
15+
typeToCheck.indexConfigs.map((config) => serializeIndexConfig(config)),
16+
);
17+
const missingIndexConfigs = baselineType.indexConfigs.filter(
18+
(baselineIndex) => !existing.has(serializeIndexConfig(baselineIndex)),
19+
);
20+
21+
if (!missingIndexConfigs.length) {
22+
return;
23+
}
24+
25+
const missingIndicesDesc = missingIndexConfigs
26+
.map((c) => print(createIndexAstNode(c)))
27+
.join(', ');
28+
29+
const location =
30+
typeToCheck.kindAstNode?.arguments?.find((a) => a.name.value === INDICES_ARG)?.value ??
31+
typeToCheck.kindAstNode;
32+
const indexIsOrPlural = missingIndexConfigs.length > 1 ? 'indices are' : 'index is';
33+
context.addMessage(
34+
ValidationMessage.suppressableCompatibilityIssue(
35+
'INDICES',
36+
`The following ${indexIsOrPlural} missing: ${missingIndicesDesc}`,
37+
typeToCheck.astNode,
38+
{ location },
39+
),
40+
);
41+
}
42+
43+
function serializeIndexConfig(indexConfig: IndexDefinitionConfig) {
44+
const unique = indexConfig.unique ?? false;
45+
const sparse = indexConfig.sparse ?? unique;
46+
return JSON.stringify({
47+
fields: indexConfig.fields,
48+
sparse,
49+
unique,
50+
});
51+
}
52+
53+
function createIndexAstNode(indexConfig: IndexDefinitionConfig): ObjectValueNode {
54+
const unique = indexConfig.unique ?? false;
55+
const sparse = indexConfig.sparse ?? unique;
56+
57+
const fields: ObjectFieldNode[] = [
58+
{
59+
kind: Kind.OBJECT_FIELD,
60+
name: { kind: Kind.NAME, value: 'fields' },
61+
value: {
62+
kind: Kind.LIST,
63+
values: indexConfig.fields.map(
64+
(value): StringValueNode => ({ kind: Kind.STRING, value }),
65+
),
66+
},
67+
},
68+
];
69+
70+
if (unique) {
71+
fields.push({
72+
kind: Kind.OBJECT_FIELD,
73+
name: { kind: Kind.NAME, value: 'unique' },
74+
value: { kind: Kind.BOOLEAN, value: true },
75+
});
76+
}
77+
78+
// sparse defaults to unique
79+
if (sparse !== unique) {
80+
fields.push({
81+
kind: Kind.OBJECT_FIELD,
82+
name: { kind: Kind.NAME, value: 'sparse' },
83+
value: { kind: Kind.BOOLEAN, value: sparse },
84+
});
85+
}
86+
87+
return {
88+
kind: Kind.OBJECT,
89+
fields,
90+
};
91+
}

src/model/compatibility-check/check-root-entity-type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ValidationContext } from '../validation';
33
import { checkBusinessObject } from './check-business-object';
44
import { checkTtl } from './check-ttl';
55
import { checkFlexSearchOnType } from './check-flex-search-on-type';
6+
import { checkIndices } from './check-indices';
67

78
export function checkRootEntityType(
89
typeToCheck: RootEntityType,
@@ -12,4 +13,5 @@ export function checkRootEntityType(
1213
checkBusinessObject(typeToCheck, baselineType, context);
1314
checkTtl(typeToCheck, baselineType, context);
1415
checkFlexSearchOnType(typeToCheck, baselineType, context);
16+
checkIndices(typeToCheck, baselineType, context);
1517
}

0 commit comments

Comments
 (0)