mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 00:39:49 +00:00
feat(history): diff schemas to get unknown attributes (#19849)
This commit is contained in:
parent
40b339395f
commit
1c2f2708ff
@ -1 +1,11 @@
|
|||||||
export const HISTORY_VERSION_UID = 'plugin::content-manager.history-version';
|
export const HISTORY_VERSION_UID = 'plugin::content-manager.history-version';
|
||||||
|
export const FIELDS_TO_IGNORE = [
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'publishedAt',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
'locale',
|
||||||
|
'strapi_stage',
|
||||||
|
'strapi_assignee',
|
||||||
|
];
|
||||||
|
@ -35,6 +35,7 @@ const mockStrapi = {
|
|||||||
i18n: {
|
i18n: {
|
||||||
service: jest.fn(() => ({
|
service: jest.fn(() => ({
|
||||||
getDefaultLocale: jest.fn().mockReturnValue('en'),
|
getDefaultLocale: jest.fn().mockReturnValue('en'),
|
||||||
|
find: jest.fn(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -95,6 +96,7 @@ describe('history-version service', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('boostrap', () => {
|
||||||
it('inits service only once', () => {
|
it('inits service only once', () => {
|
||||||
historyService.bootstrap();
|
historyService.bootstrap();
|
||||||
historyService.bootstrap();
|
historyService.bootstrap();
|
||||||
@ -158,6 +160,20 @@ describe('history-version service', () => {
|
|||||||
expect(next).toHaveBeenCalledWith(context);
|
expect(next).toHaveBeenCalledWith(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create a cron job that runs once a day', async () => {
|
||||||
|
// @ts-expect-error - this is a mock
|
||||||
|
const mockScheduleJob = scheduleJob.mockImplementationOnce(
|
||||||
|
jest.fn((rule, callback) => callback())
|
||||||
|
);
|
||||||
|
|
||||||
|
await historyService.bootstrap();
|
||||||
|
|
||||||
|
expect(mockScheduleJob).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockScheduleJob).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createVersion', () => {
|
||||||
it('creates a history version with the author', async () => {
|
it('creates a history version with the author', async () => {
|
||||||
jest.useFakeTimers().setSystemTime(fakeDate);
|
jest.useFakeTimers().setSystemTime(fakeDate);
|
||||||
|
|
||||||
@ -215,16 +231,5 @@ describe('history-version service', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a cron job that runs once a day', async () => {
|
|
||||||
// @ts-expect-error - this is a mock
|
|
||||||
const mockScheduleJob = scheduleJob.mockImplementationOnce(
|
|
||||||
jest.fn((rule, callback) => callback())
|
|
||||||
);
|
|
||||||
|
|
||||||
await historyService.bootstrap();
|
|
||||||
|
|
||||||
expect(mockScheduleJob).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockScheduleJob).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
import { getSchemaAttributesDiff } from '../utils';
|
||||||
|
|
||||||
|
describe('history-version service utils', () => {
|
||||||
|
describe('getSchemaAttributesDiff', () => {
|
||||||
|
it('should return a diff', () => {
|
||||||
|
const versionSchema = {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
someOtherField: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const contentTypeSchema = {
|
||||||
|
renamed: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
newField: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
someOtherField: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error ignore
|
||||||
|
const { added, removed } = getSchemaAttributesDiff(versionSchema, contentTypeSchema);
|
||||||
|
|
||||||
|
expect(added).toEqual({
|
||||||
|
renamed: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
newField: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(removed).toEqual({
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return a diff', () => {
|
||||||
|
const versionSchema = {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const contentTypeSchema = {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error ignore
|
||||||
|
const { added, removed } = getSchemaAttributesDiff(versionSchema, contentTypeSchema);
|
||||||
|
|
||||||
|
expect(added).toEqual({});
|
||||||
|
expect(removed).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -3,8 +3,9 @@ import { omit, pick } from 'lodash/fp';
|
|||||||
|
|
||||||
import { scheduleJob } from 'node-schedule';
|
import { scheduleJob } from 'node-schedule';
|
||||||
|
|
||||||
import { HISTORY_VERSION_UID } from '../constants';
|
import { FIELDS_TO_IGNORE, HISTORY_VERSION_UID } from '../constants';
|
||||||
import type { HistoryVersions } from '../../../../shared/contracts';
|
import type { HistoryVersions } from '../../../../shared/contracts';
|
||||||
|
import { getSchemaAttributesDiff } from './utils';
|
||||||
|
|
||||||
const DEFAULT_RETENTION_DAYS = 90;
|
const DEFAULT_RETENTION_DAYS = 90;
|
||||||
|
|
||||||
@ -104,24 +105,13 @@ const createHistoryService = ({ strapi }: { strapi: Core.LoadedStrapi }) => {
|
|||||||
});
|
});
|
||||||
const status = await getVersionStatus(contentTypeUid, document);
|
const status = await getVersionStatus(contentTypeUid, document);
|
||||||
|
|
||||||
const fieldsToIgnore = [
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
'publishedAt',
|
|
||||||
'createdBy',
|
|
||||||
'updatedBy',
|
|
||||||
'locale',
|
|
||||||
'strapi_stage',
|
|
||||||
'strapi_assignee',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Prevent creating a history version for an action that wasn't actually executed
|
// Prevent creating a history version for an action that wasn't actually executed
|
||||||
await strapi.db.transaction(async ({ onCommit }) => {
|
await strapi.db.transaction(async ({ onCommit }) => {
|
||||||
onCommit(() => {
|
onCommit(() => {
|
||||||
this.createVersion({
|
this.createVersion({
|
||||||
contentType: contentTypeUid,
|
contentType: contentTypeUid,
|
||||||
data: omit(fieldsToIgnore, document),
|
data: omit(FIELDS_TO_IGNORE, document),
|
||||||
schema: omit(fieldsToIgnore, strapi.contentType(contentTypeUid).attributes),
|
schema: omit(FIELDS_TO_IGNORE, strapi.contentType(contentTypeUid).attributes),
|
||||||
relatedDocumentId: documentContext.documentId,
|
relatedDocumentId: documentContext.documentId,
|
||||||
locale,
|
locale,
|
||||||
status,
|
status,
|
||||||
@ -183,7 +173,20 @@ const createHistoryService = ({ strapi }: { strapi: Core.LoadedStrapi }) => {
|
|||||||
getLocaleDictionary(),
|
getLocaleDictionary(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const sanitizedResults = results.map((result) => ({
|
const versionsWithMeta = results.map((version) => {
|
||||||
|
const { added, removed } = getSchemaAttributesDiff(
|
||||||
|
version.schema,
|
||||||
|
strapi.getModel(params.contentType).attributes
|
||||||
|
);
|
||||||
|
const hasSchemaDiff = Object.keys(added).length > 0 || Object.keys(removed).length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...version,
|
||||||
|
...(hasSchemaDiff ? { meta: { unknownAttributes: { added, removed } } } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanitizedResults = versionsWithMeta.map((result) => ({
|
||||||
...result,
|
...result,
|
||||||
locale: result.locale ? localeDictionary[result.locale] : null,
|
locale: result.locale ? localeDictionary[result.locale] : null,
|
||||||
createdBy: result.createdBy
|
createdBy: result.createdBy
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { difference, omit } from 'lodash/fp';
|
||||||
|
import { Schema } from '@strapi/types';
|
||||||
|
import { CreateHistoryVersion } from '../../../../shared/contracts/history-versions';
|
||||||
|
import { FIELDS_TO_IGNORE } from '../constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the difference between the version schema and the content type schema
|
||||||
|
* Returns the attributes with their original shape
|
||||||
|
*/
|
||||||
|
export const getSchemaAttributesDiff = (
|
||||||
|
versionSchemaAttributes: CreateHistoryVersion['schema'],
|
||||||
|
contentTypeSchemaAttributes: Schema.Attributes
|
||||||
|
) => {
|
||||||
|
// Omit the same fields that were omitted when creating a history version
|
||||||
|
const sanitizedContentTypeSchemaAttributes = omit(
|
||||||
|
FIELDS_TO_IGNORE,
|
||||||
|
contentTypeSchemaAttributes
|
||||||
|
) as CreateHistoryVersion['schema'];
|
||||||
|
|
||||||
|
const reduceDifferenceToAttributesObject = (
|
||||||
|
diffKeys: string[],
|
||||||
|
source: CreateHistoryVersion['schema']
|
||||||
|
) => {
|
||||||
|
return diffKeys.reduce<CreateHistoryVersion['schema']>((previousAttributesObject, diffKey) => {
|
||||||
|
previousAttributesObject[diffKey] = source[diffKey];
|
||||||
|
|
||||||
|
return previousAttributesObject;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionSchemaKeys = Object.keys(versionSchemaAttributes);
|
||||||
|
const contentTypeSchemaAttributesKeys = Object.keys(sanitizedContentTypeSchemaAttributes);
|
||||||
|
// The attribute is new if it's on the content type schema but not on the version schema
|
||||||
|
const uniqueToContentType = difference(contentTypeSchemaAttributesKeys, versionSchemaKeys);
|
||||||
|
const added = reduceDifferenceToAttributesObject(
|
||||||
|
uniqueToContentType,
|
||||||
|
sanitizedContentTypeSchemaAttributes
|
||||||
|
);
|
||||||
|
// The attribute was removed or renamed if it's on the version schema but not on the content type schema
|
||||||
|
const uniqueToVersion = difference(versionSchemaKeys, contentTypeSchemaAttributesKeys);
|
||||||
|
const removed = reduceDifferenceToAttributesObject(uniqueToVersion, versionSchemaAttributes);
|
||||||
|
|
||||||
|
return { added, removed };
|
||||||
|
};
|
@ -31,6 +31,12 @@ export interface HistoryVersionDataResponse extends Omit<CreateHistoryVersion, '
|
|||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
locale: Locale | null;
|
locale: Locale | null;
|
||||||
|
meta?: {
|
||||||
|
unknownAttributes?: {
|
||||||
|
added: Record<string, unknown>;
|
||||||
|
removed: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export to prevent the TS "cannot be named" error in the history service
|
// Export to prevent the TS "cannot be named" error in the history service
|
||||||
|
Loading…
x
Reference in New Issue
Block a user