init
This commit is contained in:
5
server/src/bootstrap.ts
Normal file
5
server/src/bootstrap.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Core } from '@strapi/strapi';
|
||||
|
||||
const bootstrap = ({ strapi }: { strapi: Core.Strapi }) => {};
|
||||
|
||||
export default bootstrap;
|
||||
10
server/src/config/index.ts
Normal file
10
server/src/config/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Config } from '../types';
|
||||
|
||||
export default {
|
||||
default: () => ({ contentVersionLimit: 50 }),
|
||||
validator: (config: Config) => {
|
||||
if (typeof config.contentVersionLimit !== 'number') {
|
||||
throw new Error('contentVersionLimit has to be a boolean');
|
||||
}
|
||||
},
|
||||
};
|
||||
38
server/src/content-types/content-version.ts
Normal file
38
server/src/content-types/content-version.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
kind: 'collectionType',
|
||||
collectionName: 'content-version',
|
||||
info: {
|
||||
singularName: 'content-version', // kebab-case mandatory
|
||||
pluralName: 'content-versions', // kebab-case mandatory
|
||||
displayName: 'Версия контента',
|
||||
},
|
||||
pluginOptions: {
|
||||
'content-manager': {
|
||||
visible: false,
|
||||
},
|
||||
'content-type-builder': {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
draftAndPublish: false,
|
||||
},
|
||||
attributes: {
|
||||
contentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'datetime',
|
||||
default: () => new Date(),
|
||||
},
|
||||
},
|
||||
};
|
||||
5
server/src/content-types/index.ts
Normal file
5
server/src/content-types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import contentVersion from './content-version';
|
||||
|
||||
export default {
|
||||
'content-version': { schema: contentVersion },
|
||||
};
|
||||
5
server/src/controllers/index.ts
Normal file
5
server/src/controllers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import version from './version';
|
||||
|
||||
export default {
|
||||
version,
|
||||
};
|
||||
21
server/src/controllers/version.ts
Normal file
21
server/src/controllers/version.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Core } from '@strapi/strapi';
|
||||
import { ContentVersionService } from '../services';
|
||||
import { getService } from '../utils';
|
||||
|
||||
const versionController = ({ strapi }: { strapi: Core.Strapi }) => ({
|
||||
async find(ctx) {
|
||||
const { getVersions } = getService<ContentVersionService>('contentVersion');
|
||||
const { model, contentId } = ctx.params;
|
||||
|
||||
return getVersions(contentId, model);
|
||||
},
|
||||
async getVersionFormData(ctx) {
|
||||
const { getVersionFormData } = getService<ContentVersionService>('contentVersion');
|
||||
const { versionId } = ctx.params;
|
||||
const locale = ctx.request.query['locale'];
|
||||
|
||||
return getVersionFormData(versionId, locale);
|
||||
},
|
||||
});
|
||||
|
||||
export default versionController;
|
||||
7
server/src/destroy.ts
Normal file
7
server/src/destroy.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Core } from '@strapi/strapi';
|
||||
|
||||
const destroy = ({ strapi }: { strapi: Core.Strapi }) => {
|
||||
// destroy phase
|
||||
};
|
||||
|
||||
export default destroy;
|
||||
30
server/src/index.ts
Normal file
30
server/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Application methods
|
||||
*/
|
||||
import bootstrap from './bootstrap';
|
||||
import destroy from './destroy';
|
||||
import register from './register';
|
||||
|
||||
/**
|
||||
* Plugin server methods
|
||||
*/
|
||||
import config from './config';
|
||||
import contentTypes from './content-types';
|
||||
import controllers from './controllers';
|
||||
import middlewares from './middlewares';
|
||||
import policies from './policies';
|
||||
import routes from './routes';
|
||||
import services from './services';
|
||||
|
||||
export default {
|
||||
register,
|
||||
bootstrap,
|
||||
destroy,
|
||||
config,
|
||||
controllers,
|
||||
routes,
|
||||
services,
|
||||
contentTypes,
|
||||
policies,
|
||||
middlewares,
|
||||
};
|
||||
36
server/src/middlewares/delete-with-versions.ts
Normal file
36
server/src/middlewares/delete-with-versions.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Core, Internal, UID } from '@strapi/strapi';
|
||||
import { get } from 'radash';
|
||||
import { getService } from '../utils';
|
||||
import { ContentVersionService } from '../services';
|
||||
|
||||
type RequestParams = {
|
||||
model: UID.Schema;
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export const deleteWithVersions: Core.MiddlewareHandler = async (ctx, next) => {
|
||||
const { model, documentId } = get<RequestParams>(ctx, 'request.params');
|
||||
const modelDef = strapi.getModel(model);
|
||||
const contentTypeService = getService('contentTypes');
|
||||
const contentVersionService = getService<ContentVersionService>('contentVersion');
|
||||
const modelDocumentService = strapi.documents(model as Internal.UID.ContentType);
|
||||
|
||||
if (!contentTypeService.isVersionedContentType(modelDef)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
await strapi.db.transaction(async () => {
|
||||
const ids = await modelDocumentService
|
||||
.findMany({
|
||||
fields: ['id'],
|
||||
filters: {
|
||||
documentId,
|
||||
},
|
||||
locale: ctx.request.query.locale as string | undefined,
|
||||
})
|
||||
.then((data) => data.map(({ id }) => id));
|
||||
|
||||
await contentVersionService.deleteVersionsForContent(ids);
|
||||
return next();
|
||||
});
|
||||
};
|
||||
4
server/src/middlewares/index.ts
Normal file
4
server/src/middlewares/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {};
|
||||
|
||||
export * from './publish-with-version';
|
||||
export * from './delete-with-versions';
|
||||
41
server/src/middlewares/publish-with-version.ts
Normal file
41
server/src/middlewares/publish-with-version.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Core, Internal, UID } from '@strapi/strapi';
|
||||
import { get } from 'radash';
|
||||
import { getDeepPopulate, getService } from '../utils';
|
||||
import { CreateVersionInput, ContentVersionService } from '../services';
|
||||
|
||||
type RequestParams = {
|
||||
model: UID.Schema;
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export const publishWithVersionMiddleware: Core.MiddlewareHandler = async (ctx, next) => {
|
||||
const { model, documentId } = get<RequestParams>(ctx, 'request.params');
|
||||
const modelDef = strapi.getModel(model);
|
||||
const contentTypeService = getService('contentTypes');
|
||||
const contentVersionService = getService<ContentVersionService>('contentVersion');
|
||||
const modelDocumentService = strapi.documents(model as Internal.UID.ContentType);
|
||||
|
||||
if (!contentTypeService.isVersionedContentType(modelDef)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
await strapi.db.transaction(async () => {
|
||||
await next();
|
||||
|
||||
const deepPopulate = await getDeepPopulate(model);
|
||||
const data = await modelDocumentService.findOne({
|
||||
documentId,
|
||||
locale: ctx.request.body.locale,
|
||||
populate: deepPopulate,
|
||||
});
|
||||
|
||||
if (!data) throw new Error(`Empty entry for documentid ${documentId}`);
|
||||
|
||||
const versionEntry: CreateVersionInput = {
|
||||
model: model,
|
||||
contentId: data.id,
|
||||
data,
|
||||
};
|
||||
await contentVersionService.createVersion(versionEntry);
|
||||
});
|
||||
};
|
||||
1
server/src/pluginId.ts
Normal file
1
server/src/pluginId.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PLUGIN_ID = 'content-versioning';
|
||||
1
server/src/policies/index.ts
Normal file
1
server/src/policies/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
30
server/src/register.ts
Normal file
30
server/src/register.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Core } from '@strapi/strapi';
|
||||
import { deleteWithVersions, publishWithVersionMiddleware } from './middlewares';
|
||||
|
||||
const addDeleteWithVersionMiddleware = (strapi: Core.Strapi) =>
|
||||
strapi.server.router.use('/content-manager/collection-types/:model/:documentId', (ctx, next) => {
|
||||
if (ctx.method === 'DELETE') {
|
||||
return deleteWithVersions(ctx, next);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
const addPublishWithVersionMiddleware = (strapi: Core.Strapi) =>
|
||||
strapi.server.router.use(
|
||||
'/content-manager/collection-types/:model/:documentId/actions/publish',
|
||||
(ctx, next) => {
|
||||
if (ctx.method === 'POST') {
|
||||
return publishWithVersionMiddleware(ctx, next);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
const register = ({ strapi }: { strapi: Core.Strapi }) => {
|
||||
addPublishWithVersionMiddleware(strapi);
|
||||
addDeleteWithVersionMiddleware(strapi);
|
||||
};
|
||||
|
||||
export default register;
|
||||
12
server/src/routes/index.ts
Normal file
12
server/src/routes/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export default [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/version/:model/:contentId',
|
||||
handler: 'version.find',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/version-form-data/:versionId',
|
||||
handler: 'version.getVersionFormData',
|
||||
},
|
||||
];
|
||||
10
server/src/services/content-types.ts
Normal file
10
server/src/services/content-types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { get } from 'radash';
|
||||
|
||||
const hasVersionedOption = (modelOrAttribute) =>
|
||||
get(modelOrAttribute, 'pluginOptions.versions.versioned', false);
|
||||
|
||||
export const isVersionedContentType = (model) => hasVersionedOption(model);
|
||||
|
||||
export default {
|
||||
isVersionedContentType,
|
||||
};
|
||||
110
server/src/services/content-version.ts
Normal file
110
server/src/services/content-version.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Core } from '@strapi/strapi';
|
||||
import { PLUGIN_ID } from '../pluginId';
|
||||
import { Config, ContentVersion } from '../types';
|
||||
import { getConfig, getDeepPopulate, prepareVersionFormData } from '../utils';
|
||||
import { get } from 'radash';
|
||||
|
||||
export const CONTENT_VERSION_ID = `${PLUGIN_ID}.content-version`;
|
||||
export const PLUGIN_CONTENT_VERSION_ID = `plugin::${CONTENT_VERSION_ID}`;
|
||||
|
||||
export type CreateVersionInput = Pick<ContentVersion, 'contentId' | 'data' | 'model'>;
|
||||
|
||||
export type ContentVersionService = ReturnType<typeof contentVersionService>;
|
||||
|
||||
const contentVersionService = ({ strapi }: { strapi: Core.Strapi }) => ({
|
||||
async createVersion(data: CreateVersionInput) {
|
||||
return await strapi.db.transaction(async () => {
|
||||
const config = getConfig();
|
||||
const contentVersionLimit = config<Config['contentVersionLimit']>('contentVersionLimit');
|
||||
const newVersion = await strapi.query(PLUGIN_CONTENT_VERSION_ID).create({ data });
|
||||
const allVersions = await strapi.query(PLUGIN_CONTENT_VERSION_ID).findMany({
|
||||
select: 'id',
|
||||
where: { contentId: data.contentId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (allVersions.length > contentVersionLimit) {
|
||||
await strapi.query(PLUGIN_CONTENT_VERSION_ID).deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
$in: allVersions.slice(contentVersionLimit).map((item) => get(item, 'id')),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return newVersion;
|
||||
});
|
||||
},
|
||||
|
||||
async getVersions(
|
||||
contentId: CreateVersionInput['contentId'],
|
||||
model: CreateVersionInput['model']
|
||||
) {
|
||||
return strapi.query(PLUGIN_CONTENT_VERSION_ID).findMany({
|
||||
select: ['id', 'contentId', 'documentId', 'createdAt'],
|
||||
where: { contentId, model },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
async getVersion(versionId: string) {
|
||||
return strapi.query(PLUGIN_CONTENT_VERSION_ID).findOne({
|
||||
where: { id: versionId },
|
||||
});
|
||||
},
|
||||
|
||||
async deleteVersionsForContent(contentIds: CreateVersionInput['contentId'][]) {
|
||||
return strapi.query(PLUGIN_CONTENT_VERSION_ID).deleteMany({
|
||||
where: {
|
||||
contentId: {
|
||||
$in: contentIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getVersionFormData(versionId: string, locale: string = null) {
|
||||
const version: ContentVersion = await strapi
|
||||
.query(PLUGIN_CONTENT_VERSION_ID)
|
||||
.findOne({ where: { id: versionId } });
|
||||
|
||||
if (!version) throw new Error(`version with id ${versionId} not found`);
|
||||
|
||||
const { model, data, contentId } = version;
|
||||
const contentTypesService = strapi.plugin('content-manager').services['content-types'];
|
||||
|
||||
const contentTypeSchema = strapi.getModel(model);
|
||||
const componentsSchema = strapi.components;
|
||||
const deepPopulate = await getDeepPopulate(model);
|
||||
const [contentTypeSettings, componentsSettings, currentData] = await Promise.all([
|
||||
contentTypesService.findConfiguration(contentTypeSchema),
|
||||
contentTypesService.findComponentsConfigurations(contentTypeSchema),
|
||||
strapi.db
|
||||
.query(model)
|
||||
.findOne({ where: { id: contentId }, select: ['documentId'] })
|
||||
.then(({ documentId }) =>
|
||||
//@ts-expect-error
|
||||
strapi.documents(model).findOne({
|
||||
populate: deepPopulate,
|
||||
locale,
|
||||
documentId,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
const result = await prepareVersionFormData({
|
||||
schema: contentTypeSchema,
|
||||
configuration: contentTypeSettings,
|
||||
componentsConfiguration: componentsSettings,
|
||||
componentsSchema,
|
||||
versionData: data,
|
||||
currentData,
|
||||
locale,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export default contentVersionService;
|
||||
9
server/src/services/index.ts
Normal file
9
server/src/services/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import contentTypes from './content-types';
|
||||
import contentVersion from './content-version';
|
||||
|
||||
export { type ContentVersionService, CreateVersionInput } from './content-version';
|
||||
|
||||
export default {
|
||||
contentVersion,
|
||||
contentTypes,
|
||||
};
|
||||
46
server/src/types.ts
Normal file
46
server/src/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { UID } from '@strapi/strapi';
|
||||
|
||||
export type ContentVersion<T = Record<string, unknown>> = {
|
||||
documentId: string;
|
||||
contentId: number;
|
||||
model: UID.Schema;
|
||||
createdAt: string;
|
||||
data: T;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type FormRelationFieldAttributes = {
|
||||
id: number;
|
||||
documentId: string;
|
||||
locale: string | null;
|
||||
};
|
||||
|
||||
export type FormRelationConnection = FormRelationFieldAttributes & {
|
||||
href?: string;
|
||||
label: string;
|
||||
apiData?: FormRelationFieldAttributes;
|
||||
};
|
||||
|
||||
export type FormRelation = {
|
||||
connect?: FormRelationConnection[];
|
||||
disconnect?: FormRelationConnection[];
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
edit: {
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
visible: boolean;
|
||||
editable: boolean;
|
||||
mainField?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Configuration = {
|
||||
metadatas: Record<string, Metadata>;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
contentVersionLimit: number;
|
||||
};
|
||||
9
server/src/utils/getRelationLabel.ts
Normal file
9
server/src/utils/getRelationLabel.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const getRelationLabel = (content: Record<string, unknown>, mainField?: string): string => {
|
||||
const label = mainField && content[mainField] ? content[mainField] : null;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
return label;
|
||||
}
|
||||
|
||||
return content.documentId as string;
|
||||
};
|
||||
16
server/src/utils/index.ts
Normal file
16
server/src/utils/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Core, UID } from '@strapi/strapi';
|
||||
import { PLUGIN_ID } from '../pluginId';
|
||||
|
||||
export * from './prepareVersionFormData';
|
||||
export * from './getRelationLabel';
|
||||
|
||||
export const getService = <T = Core.Service>(name: string) =>
|
||||
strapi.plugin(PLUGIN_ID).service(name) as T;
|
||||
|
||||
export const getConfig = () => strapi.plugin(PLUGIN_ID).config;
|
||||
|
||||
export const getDeepPopulate = async (model: UID.Schema) =>
|
||||
// @ts-expect-error
|
||||
strapi.plugin('content-manager').services['populate-builder'](model).populateDeep().build();
|
||||
|
||||
export default {};
|
||||
5
server/src/utils/isLocalizedContentType.ts
Normal file
5
server/src/utils/isLocalizedContentType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UID } from '@strapi/strapi';
|
||||
|
||||
export const isLocalizedContentType = (model: UID.Schema) => {
|
||||
return strapi.plugin('i18n')?.service('content-types')?.isLocalizedContentType(model);
|
||||
};
|
||||
223
server/src/utils/prepareVersionFormData.ts
Normal file
223
server/src/utils/prepareVersionFormData.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import {Schema, UID} from '@strapi/strapi';
|
||||
import {chain, get, isArray, isEmpty, isEqual, isNumber, pick} from 'radash';
|
||||
import {Configuration, Metadata, FormRelationConnection, FormRelation} from '../types';
|
||||
import {getRelationLabel} from './getRelationLabel';
|
||||
import {withTempKeys} from './withTempKeys';
|
||||
|
||||
type PrepareFormDataProps = {
|
||||
currentData: unknown;
|
||||
versionData: unknown;
|
||||
schema: Schema.ContentType | Schema.Component;
|
||||
componentsSchema?: Schema.Components;
|
||||
configuration: Configuration;
|
||||
componentsConfiguration?: Record<UID.Component, Configuration>;
|
||||
locale: string | null;
|
||||
};
|
||||
|
||||
type RelationValue = {id: number; documentId: string} & Record<string, unknown>;
|
||||
|
||||
type PrepareRelationProps = {
|
||||
metadata?: Metadata;
|
||||
currentData?: RelationValue | RelationValue[];
|
||||
versionData?: RelationValue | RelationValue[];
|
||||
locale: string | null;
|
||||
};
|
||||
|
||||
const toArray = <T>(data?: T | T[]) => (isArray(data) ? data : [data]);
|
||||
const isEqualIds = (prev?: unknown, curr?: unknown) => {
|
||||
const currId = get(curr, 'id')
|
||||
const prevId = get(prev, 'id')
|
||||
|
||||
return isNumber(prevId) && isNumber(currId) && isEqual(currId, prevId)
|
||||
}
|
||||
|
||||
const makeConnection = (
|
||||
relation: RelationValue,
|
||||
locale: string | null,
|
||||
mainField?: string
|
||||
): FormRelationConnection => {
|
||||
const mainIdentifiers = pick(relation, ['documentId', 'id']);
|
||||
|
||||
return {
|
||||
...mainIdentifiers,
|
||||
apiData: {
|
||||
...mainIdentifiers,
|
||||
locale,
|
||||
},
|
||||
[mainField ?? 'documentId']: relation[mainField ?? 'documentId'],
|
||||
label: getRelationLabel(relation, mainField),
|
||||
locale,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchFreshRelationData = (
|
||||
data: RelationValue | RelationValue[],
|
||||
schema: Schema.Attribute.Relation,
|
||||
locale: string | null,
|
||||
metadata?: Metadata
|
||||
) => {
|
||||
const payload = data
|
||||
? toArray(data)
|
||||
.map((item: RelationValue) => get(item, 'id'))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return !isEmpty(payload)
|
||||
? strapi
|
||||
// @ts-expect-error
|
||||
.documents(schema.target)
|
||||
.findMany({
|
||||
locale,
|
||||
filters: {id: {$in: payload}},
|
||||
fields: ['id', 'documentId', metadata?.edit?.mainField].filter(Boolean),
|
||||
})
|
||||
: [];
|
||||
};
|
||||
|
||||
const notIncludesRelation = (oldVal: RelationValue[]) => (newVal: RelationValue) =>
|
||||
oldVal.every(({id}) => newVal.id !== id);
|
||||
|
||||
const prepareRelation = ({
|
||||
currentData = [],
|
||||
versionData = [],
|
||||
metadata,
|
||||
locale,
|
||||
}: PrepareRelationProps): FormRelation => {
|
||||
const currentRelationsArray = toArray(currentData).filter((relation) => !!relation?.id);
|
||||
const versionRelationArray = toArray(versionData).filter((relation) => !!relation?.id);
|
||||
|
||||
if (isEmpty(currentData) && isEmpty(versionData)) return {connect: [], disconnect: []};
|
||||
|
||||
const relationsToConnect = versionRelationArray.filter(
|
||||
notIncludesRelation(currentRelationsArray)
|
||||
);
|
||||
const relationsToDisconnect = currentRelationsArray.filter(
|
||||
notIncludesRelation(versionRelationArray)
|
||||
);
|
||||
|
||||
const mainField = get<string | undefined>(metadata, 'edit.mainField');
|
||||
|
||||
const connect: FormRelationConnection[] = relationsToConnect.map((relation) =>
|
||||
makeConnection(relation, locale, mainField)
|
||||
);
|
||||
const disconnect: FormRelationConnection[] = relationsToDisconnect.map((relation) =>
|
||||
makeConnection(relation, locale, mainField)
|
||||
);
|
||||
|
||||
return {connect, disconnect};
|
||||
};
|
||||
|
||||
export const prepareVersionFormData = async ({
|
||||
componentsConfiguration,
|
||||
componentsSchema,
|
||||
locale,
|
||||
...otherProps
|
||||
}: PrepareFormDataProps) => {
|
||||
const _prepareVersionFormData = async ({
|
||||
schema,
|
||||
configuration,
|
||||
currentData,
|
||||
versionData,
|
||||
}: Omit<PrepareFormDataProps, 'componentsSchema' | 'componentsConfiguration' | 'locale'>) => {
|
||||
if (!schema || isEmpty(schema)) throw new Error('Schema was not provided');
|
||||
const result = pick(versionData as any, ['__component', isEqualIds(currentData, versionData) && 'id']);
|
||||
|
||||
for (const [attributeKey, attributeValue] of Object.entries(schema.attributes)) {
|
||||
if (attributeValue.writable === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (attributeValue.type) {
|
||||
case 'relation':
|
||||
const relationConfiguration = configuration?.metadatas?.[attributeKey];
|
||||
const freshVersionRelations = await fetchFreshRelationData(
|
||||
get(versionData, attributeKey),
|
||||
attributeValue,
|
||||
locale,
|
||||
relationConfiguration
|
||||
);
|
||||
|
||||
const relationField = prepareRelation({
|
||||
currentData: get(currentData, attributeKey),
|
||||
versionData: freshVersionRelations,
|
||||
metadata: relationConfiguration,
|
||||
locale,
|
||||
});
|
||||
|
||||
Object.assign(result, {[attributeKey]: relationField});
|
||||
break;
|
||||
case 'dynamiczone':
|
||||
const currentDynamicZone = get<any[]>(currentData, attributeKey);
|
||||
const versionDynamicZone = get<any[]>(versionData, attributeKey).filter(
|
||||
({__component}) => !!componentsSchema[__component]
|
||||
);
|
||||
const dynamicZoneField = await Promise.all(
|
||||
versionDynamicZone.map((data) =>
|
||||
_prepareVersionFormData({
|
||||
currentData: currentDynamicZone?.find(
|
||||
({__component}) => __component === data.__component
|
||||
),
|
||||
versionData: data,
|
||||
schema: componentsSchema[data.__component],
|
||||
configuration: componentsConfiguration?.[data.__component],
|
||||
})
|
||||
)
|
||||
).then(chain((res) => res.filter((item) => !isEmpty(item)), withTempKeys));
|
||||
|
||||
if (!isEmpty(dynamicZoneField)) {
|
||||
Object.assign(result, {[attributeKey]: dynamicZoneField});
|
||||
}
|
||||
break;
|
||||
case 'component':
|
||||
const component = get<string>(attributeValue, 'component');
|
||||
const versionComponent = get(versionData, attributeKey);
|
||||
|
||||
if (!versionComponent || isEmpty(versionComponent)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const request = {
|
||||
schema: componentsSchema?.[component],
|
||||
configuration: componentsConfiguration?.[component],
|
||||
locale,
|
||||
};
|
||||
|
||||
const componentField = attributeValue.repeatable
|
||||
? await Promise.all(
|
||||
toArray(versionComponent).map((data: any) =>
|
||||
_prepareVersionFormData({
|
||||
...request,
|
||||
versionData: data,
|
||||
currentData: toArray(currentData?.[attributeKey]).find(
|
||||
chain(
|
||||
(item) => get(item, 'id'),
|
||||
(id) => isEqual(id, data.id)
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
).then(withTempKeys)
|
||||
: await _prepareVersionFormData({
|
||||
...request,
|
||||
versionData: versionComponent,
|
||||
currentData: currentData?.[attributeKey],
|
||||
});
|
||||
|
||||
if (!isEmpty(componentField)) {
|
||||
Object.assign(result, {
|
||||
[attributeKey]: componentField,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (versionData?.[attributeKey] !== undefined) {
|
||||
Object.assign(result, {[attributeKey]: versionData[attributeKey]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return _prepareVersionFormData(otherProps);
|
||||
};
|
||||
7
server/src/utils/withTempKeys.ts
Normal file
7
server/src/utils/withTempKeys.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { generateNKeysBetween } from 'fractional-indexing';
|
||||
import { assign } from 'radash';
|
||||
|
||||
export const withTempKeys = (array: unknown[]) => {
|
||||
const keys = generateNKeysBetween(undefined, undefined, array.length);
|
||||
return array.map((item, index) => assign(item, { __temp_key__: keys[index] }));
|
||||
};
|
||||
10
server/tsconfig.build.json
Normal file
10
server/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"include": ["./src"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "../",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
8
server/tsconfig.json
Normal file
8
server/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@strapi/typescript-utils/tsconfigs/server",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "../",
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user