Skip to content

Commit

Permalink
feat: check for the presence of indices in checkCompatibility()
Browse files Browse the repository at this point in the history
  • Loading branch information
Yogu committed Sep 18, 2024
1 parent a35d203 commit a307a3c
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 1 deletion.
274 changes: 274 additions & 0 deletions spec/model/compatibility-check/check-indices.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import gql from 'graphql-tag';
import {
expectSingleCompatibilityIssue,
expectToBeValid,
} from '../implementation/validation-utils';
import { runCheck } from './utils';

describe('checkModel', () => {
describe('indices', () => {
it('accepts if there are no indices', () => {
const result = runCheck(
gql`
type Test @rootEntity @modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity {
field: String
}
`,
);
expectToBeValid(result);
});

it('accepts if there are indices but there do not need to be any', () => {
const result = runCheck(
gql`
type Test @rootEntity @modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
field: String
}
`,
);
expectToBeValid(result);
});

it('accepts if an index is required and present', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"] }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
field: String
}
`,
);
expectToBeValid(result);
});

it('accepts if an index is required and present with unique and sparse explicitly set to their default values', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"] }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test
@rootEntity(
indices: [{ fields: ["field"], unique: false, sparse: false }]
) {
field: String
}
`,
);
expectToBeValid(result);
});

it('rejects if an index is required and no index is present', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"] }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity {
field: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"]}',
);
});

it('rejects if an index is required, but it only exists with a different field', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"], unique: true }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field2"] }]) {
field: String
field2: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"], unique: true}',
);
});

it('rejects if an index is required, but it only exists with an additional field', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"], unique: true }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field", "field2"] }]) {
field: String
field2: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"], unique: true}',
);
});

it('rejects if an index is required, but it only exists with a different unique value', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"], unique: true }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field"] }]) {
field: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"], unique: true}',
);
});

it('rejects if an index is required, but it only exists with a different sparse value', () => {
const result = runCheck(
gql`
type Test
@rootEntity(indices: [{ fields: ["field"], unique: true }])
@modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type Test
@rootEntity(indices: [{ fields: ["field"], unique: true, sparse: false }]) {
field: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"], unique: true}',
);
});
});

it('accepts if multiple indices are required and present', () => {
const result = runCheck(
gql`
type Test
@rootEntity(
indices: [
{ fields: ["field1"] }
{ fields: ["field2", "field3"], unique: true }
{ fields: ["field3"], sparse: true }
]
)
@modules(in: "module1") {
field1: String @modules(all: true)
field2: String @modules(all: true)
field3: String @modules(all: true)
}
`,
gql`
type Test
@rootEntity(
indices: [
{ fields: ["field1"] }
{ fields: ["field2", "field3"], unique: true }
{ fields: ["field3"], sparse: true }
]
) {
field1: String
field2: String
field3: String
}
`,
);
expectToBeValid(result);
});

it('rejects if multiple indices are required but not all are present', () => {
const result = runCheck(
gql`
type Test
@rootEntity(
indices: [
{ fields: ["field1"] }
{ fields: ["field2", "field3"], unique: true }
{ fields: ["field3"], sparse: true }
]
)
@modules(in: "module1") {
field1: String @modules(all: true)
field2: String @modules(all: true)
field3: String @modules(all: true)
}
`,
gql`
type Test @rootEntity(indices: [{ fields: ["field1"] }]) {
field1: String
field2: String
field3: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following indices are missing: {fields: ["field2", "field3"], unique: true}, {fields: ["field3"], sparse: true}',
);
});

it('rejects if a field should be @unique but is not', () => {
const result = runCheck(
gql`
type Test @rootEntity @modules(in: "module1", includeAllFields: true) {
field: String @unique
}
`,
gql`
type Test @rootEntity {
field: String
}
`,
);
expectSingleCompatibilityIssue(
result,
'The following index is missing: {fields: ["field"], unique: true}',
);
});
});
91 changes: 91 additions & 0 deletions src/model/compatibility-check/check-indices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { RootEntityType } from '../implementation';
import { ValidationContext, ValidationMessage } from '../validation';
import { IndexDefinitionConfig } from '../config';
import { Kind, ObjectFieldNode, ObjectValueNode, print, StringValueNode } from 'graphql';
import { INDICES_ARG } from '../../schema/constants';

export function checkIndices(
typeToCheck: RootEntityType,
baselineType: RootEntityType,
context: ValidationContext,
) {
// use index config instead of indices because there is a transformation step that changes
// indices and also adds new indices. It would be confusing report issues for these.
const existing = new Set(
typeToCheck.indexConfigs.map((config) => serializeIndexConfig(config)),
);
const missingIndexConfigs = baselineType.indexConfigs.filter(
(baselineIndex) => !existing.has(serializeIndexConfig(baselineIndex)),
);

if (!missingIndexConfigs.length) {
return;
}

const missingIndicesDesc = missingIndexConfigs
.map((c) => print(createIndexAstNode(c)))
.join(', ');

const location =
typeToCheck.kindAstNode?.arguments?.find((a) => a.name.value === INDICES_ARG)?.value ??
typeToCheck.kindAstNode;
const indexIsOrPlural = missingIndexConfigs.length > 1 ? 'indices are' : 'index is';
context.addMessage(
ValidationMessage.suppressableCompatibilityIssue(
'INDICES',
`The following ${indexIsOrPlural} missing: ${missingIndicesDesc}`,
typeToCheck.astNode,
{ location },
),
);
}

function serializeIndexConfig(indexConfig: IndexDefinitionConfig) {
const unique = indexConfig.unique ?? false;
const sparse = indexConfig.sparse ?? unique;
return JSON.stringify({
fields: indexConfig.fields,
sparse,
unique,
});
}

function createIndexAstNode(indexConfig: IndexDefinitionConfig): ObjectValueNode {
const unique = indexConfig.unique ?? false;
const sparse = indexConfig.sparse ?? unique;

const fields: ObjectFieldNode[] = [
{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: 'fields' },
value: {
kind: Kind.LIST,
values: indexConfig.fields.map(
(value): StringValueNode => ({ kind: Kind.STRING, value }),
),
},
},
];

if (unique) {
fields.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: 'unique' },
value: { kind: Kind.BOOLEAN, value: true },
});
}

// sparse defaults to unique
if (sparse !== unique) {
fields.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: 'sparse' },
value: { kind: Kind.BOOLEAN, value: sparse },
});
}

return {
kind: Kind.OBJECT,
fields,
};
}
2 changes: 2 additions & 0 deletions src/model/compatibility-check/check-root-entity-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ValidationContext } from '../validation';
import { checkBusinessObject } from './check-business-object';
import { checkTtl } from './check-ttl';
import { checkFlexSearchOnType } from './check-flex-search-on-type';
import { checkIndices } from './check-indices';

export function checkRootEntityType(
typeToCheck: RootEntityType,
Expand All @@ -12,4 +13,5 @@ export function checkRootEntityType(
checkBusinessObject(typeToCheck, baselineType, context);
checkTtl(typeToCheck, baselineType, context);
checkFlexSearchOnType(typeToCheck, baselineType, context);
checkIndices(typeToCheck, baselineType, context);
}
Loading

0 comments on commit a307a3c

Please sign in to comment.