From 044ac391651603a84efee03d8a7a9a024de4d6ad Mon Sep 17 00:00:00 2001 From: Jan Melcher Date: Mon, 9 Sep 2024 16:22:37 +0200 Subject: [PATCH] feat: add compatibility check for TTL configs --- .../compatibility-check/check-ttl.spec.ts | 138 ++++++++++++++++++ .../check-business-object.ts | 23 +++ .../check-root-entity-type.ts | 16 +- src/model/compatibility-check/check-ttl.ts | 33 +++++ .../validation/suppress/message-codes.ts | 1 + 5 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 spec/model/compatibility-check/check-ttl.spec.ts create mode 100644 src/model/compatibility-check/check-business-object.ts create mode 100644 src/model/compatibility-check/check-ttl.ts diff --git a/spec/model/compatibility-check/check-ttl.spec.ts b/spec/model/compatibility-check/check-ttl.spec.ts new file mode 100644 index 00000000..d9dc2067 --- /dev/null +++ b/spec/model/compatibility-check/check-ttl.spec.ts @@ -0,0 +1,138 @@ +import gql from 'graphql-tag'; +import { + expectSingleCompatibilityIssue, + expectToBeValid, +} from '../implementation/validation-utils'; +import { runCheck } from './utils'; +import { Project } from '../../../src/project/project'; +import { assertValidatorAcceptsAndDoesNotWarn } from '../../schema/ast-validation-modules/helpers'; + +describe('checkModel', () => { + describe('ttl', () => { + it('rejects if a type should have a TTL config', () => { + const projectToCheck = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + ]); + expectToBeValid(projectToCheck); + + const baselineProject = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + { + name: 'ttl.json', + body: JSON.stringify({ + timeToLive: [ + { + typeName: 'Test', + dateField: 'dateField', + expireAfterDays: 30, + }, + ], + }), + }, + ]); + expectToBeValid(baselineProject); + + const checkResult = projectToCheck.checkCompatibility(baselineProject); + + expectSingleCompatibilityIssue( + checkResult, + 'There should be a timeToLive configuration for type "Test".', + ); + }); + + it('rejects if a type should not have a TTL config', () => { + const projectToCheck = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + { + name: 'ttl.json', + body: JSON.stringify({ + timeToLive: [ + { + typeName: 'Test', + dateField: 'dateField', + expireAfterDays: 30, + }, + ], + }), + }, + ]); + expectToBeValid(projectToCheck); + + const baselineProject = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + ]); + expectToBeValid(baselineProject); + + const checkResult = projectToCheck.checkCompatibility(baselineProject); + + expectSingleCompatibilityIssue( + checkResult, + 'There does not need to be a timeToLive configuration for type "Test". If the timeToLive configuration is intentional, suppress this message.', + ); + }); + + it('accepts if there is a TTL config in both baseline and project to check', () => { + const projectToCheck = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + { + name: 'ttl.json', + body: JSON.stringify({ + timeToLive: [ + { + typeName: 'Test', + dateField: 'dateField', + expireAfterDays: 15, + }, + ], + }), + }, + ]); + expectToBeValid(projectToCheck); + + const baselineProject = new Project([ + gql` + type Test @rootEntity { + dateField: DateTime + } + `.loc!.source, + { + name: 'ttl.json', + body: JSON.stringify({ + timeToLive: [ + { + typeName: 'Test', + dateField: 'dateField', + expireAfterDays: 30, + }, + ], + }), + }, + ]); + expectToBeValid(baselineProject); + + const checkResult = projectToCheck.checkCompatibility(baselineProject); + + expectToBeValid(checkResult); + }); + }); +}); diff --git a/src/model/compatibility-check/check-business-object.ts b/src/model/compatibility-check/check-business-object.ts new file mode 100644 index 00000000..c705cfb3 --- /dev/null +++ b/src/model/compatibility-check/check-business-object.ts @@ -0,0 +1,23 @@ +import { ObjectType, RootEntityType } from '../implementation'; +import { ValidationContext, ValidationMessage } from '../validation'; +import { checkField } from './check-field'; +import { getRequiredBySuffix } from './describe-module-specification'; + +export function checkBusinessObject( + typeToCheck: RootEntityType, + baselineType: RootEntityType, + context: ValidationContext, +) { + if (baselineType.isBusinessObject && !typeToCheck.isBusinessObject) { + context.addMessage( + ValidationMessage.suppressableCompatibilityIssue( + 'BUSINESS_OBJECT', + `Type "${ + baselineType.name + }" needs to be decorated with @businessObject${getRequiredBySuffix(baselineType)}.`, + typeToCheck.astNode, + { location: typeToCheck.nameASTNode }, + ), + ); + } +} diff --git a/src/model/compatibility-check/check-root-entity-type.ts b/src/model/compatibility-check/check-root-entity-type.ts index bafed6d1..d0b5286d 100644 --- a/src/model/compatibility-check/check-root-entity-type.ts +++ b/src/model/compatibility-check/check-root-entity-type.ts @@ -2,22 +2,14 @@ import { ObjectType, RootEntityType } from '../implementation'; import { ValidationContext, ValidationMessage } from '../validation'; import { checkField } from './check-field'; import { getRequiredBySuffix } from './describe-module-specification'; +import { checkBusinessObject } from './check-business-object'; +import { checkTtl } from './check-ttl'; export function checkRootEntityType( typeToCheck: RootEntityType, baselineType: RootEntityType, context: ValidationContext, ) { - if (baselineType.isBusinessObject && !typeToCheck.isBusinessObject) { - context.addMessage( - ValidationMessage.suppressableCompatibilityIssue( - 'BUSINESS_OBJECT', - `Type "${ - baselineType.name - }" needs to be decorated with @businessObject${getRequiredBySuffix(baselineType)}.`, - typeToCheck.astNode, - { location: typeToCheck.nameASTNode }, - ), - ); - } + checkBusinessObject(typeToCheck, baselineType, context); + checkTtl(typeToCheck, baselineType, context); } diff --git a/src/model/compatibility-check/check-ttl.ts b/src/model/compatibility-check/check-ttl.ts new file mode 100644 index 00000000..59043548 --- /dev/null +++ b/src/model/compatibility-check/check-ttl.ts @@ -0,0 +1,33 @@ +import { ObjectType, RootEntityType } from '../implementation'; +import { ValidationContext, ValidationMessage } from '../validation'; +import { checkField } from './check-field'; +import { getRequiredBySuffix } from './describe-module-specification'; + +export function checkTtl( + typeToCheck: RootEntityType, + baselineType: RootEntityType, + context: ValidationContext, +) { + if (baselineType.timeToLiveTypes.length && !typeToCheck.timeToLiveTypes.length) { + context.addMessage( + ValidationMessage.suppressableCompatibilityIssue( + 'TTL', + `There should be a timeToLive configuration for type "${baselineType.name}"${getRequiredBySuffix(baselineType)}.`, + typeToCheck.astNode, + { location: typeToCheck.nameASTNode }, + ), + ); + } + if (!baselineType.timeToLiveTypes.length && typeToCheck.timeToLiveTypes.length) { + // The @suppress needs to be specified on the root entity definition (because there is no @suppress for yaml/json files) + // For this reason, we report the error on the type and not on the TTL config + context.addMessage( + ValidationMessage.suppressableCompatibilityIssue( + 'TTL', + `There does not need to be a timeToLive configuration for type "${baselineType.name}". If the timeToLive configuration is intentional, suppress this message.`, + typeToCheck.astNode, + { location: typeToCheck.nameASTNode }, + ), + ); + } +} diff --git a/src/model/validation/suppress/message-codes.ts b/src/model/validation/suppress/message-codes.ts index e8a387fa..5b877cb2 100644 --- a/src/model/validation/suppress/message-codes.ts +++ b/src/model/validation/suppress/message-codes.ts @@ -55,6 +55,7 @@ export const COMPATIBILITY_ISSUE_CODES = { PARENT_FIELD: 'Missing or superfluous @parent', BUSINESS_OBJECT: 'Missing or superfluous @businessObject', TYPE_KIND: 'A type declaration is of the wrong kind (e.g. root entity, value object or enum)', + TTL: 'Missing or superfluous time-to-live configuration', } as const satisfies MessageCodes; export type CompatibilityIssueCode = keyof typeof COMPATIBILITY_ISSUE_CODES;