Skip to content

Commit

Permalink
chore: separate doc & entries logic more
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrebodin committed Apr 11, 2024
1 parent c91c1a7 commit 9535601
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 113 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { curry } from 'lodash/fp';

import type { UID, Modules, Schema } from '@strapi/types';

import transforms from './transforms';

const applyTransforms = <TUID extends UID.ContentType>(
data: Modules.EntityService.Params.Data.Input<TUID>,
context: {
contentType: Schema.ContentType<TUID>;
}
) => {
const { contentType } = context;
// aliasing the type to make it easier to read
type Data = Modules.Documents.Params.Data.Input<UID.Schema>;

const applyTransforms = curry((schema: Schema.Schema, data: Data) => {
const attributeNames = Object.keys(data) as Array<keyof typeof data & string>;

for (const attributeName of attributeNames) {
const value = data[attributeName];

const attribute = contentType.attributes[attributeName];
const attribute = schema.attributes[attributeName];

if (!attribute) {
continue;
Expand All @@ -23,13 +22,13 @@ const applyTransforms = <TUID extends UID.ContentType>(
const transform = transforms[attribute.type];

if (transform) {
const attributeContext = { ...context, attributeName, attribute };
const attributeContext = { attributeName, attribute };

data[attributeName] = transform(value, attributeContext);
}
}

return data;
};
});

export { applyTransforms };
92 changes: 30 additions & 62 deletions packages/core/core/src/services/document-service/components.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import _ from 'lodash';
import { has, omit, pipe, assign } from 'lodash/fp';
import type { Struct, Utils, UID, Schema, Data, Modules } from '@strapi/types';
import { has, omit, pipe, assign, curry } from 'lodash/fp';
import type { Utils, UID, Schema, Data, Modules } from '@strapi/types';
import { contentTypes as contentTypesUtils, async, errors } from '@strapi/utils';

// type aliases for readability
type Input<T extends UID.Schema> = Modules.Documents.Params.Data.Input<T>;

type LoadedComponents<TUID extends UID.Schema> = Data.Entity<
TUID,
Schema.AttributeNamesByType<TUID, 'component' | 'dynamiczone'>
Expand All @@ -19,37 +22,19 @@ type ComponentBody = {
[key: string]: ComponentValue | DynamicZoneValue;
};

function omitComponentData(
contentType: Struct.ContentTypeSchema,
data: Modules.EntityService.Params.Data.Input<Struct.ContentTypeSchema['uid']>
): Partial<Modules.EntityService.Params.Data.Input<Struct.ContentTypeSchema['uid']>>;
function omitComponentData(
contentType: Struct.ComponentSchema,
data: Modules.EntityService.Params.Data.Input<Struct.ComponentSchema['uid']>
): Partial<Modules.EntityService.Params.Data.Input<Struct.ComponentSchema['uid']>>;
function omitComponentData(
contentType: Struct.Schema,
data: Modules.EntityService.Params.Data.Input<
Struct.ContentTypeSchema['uid'] | Struct.ComponentSchema['uid']
>
): Partial<
Modules.EntityService.Params.Data.Input<
Struct.ContentTypeSchema['uid'] | Struct.ComponentSchema['uid']
>
> {
const { attributes } = contentType;
const componentAttributes = Object.keys(attributes).filter((attributeName) =>
contentTypesUtils.isComponentAttribute(attributes[attributeName])
);
const omitComponentData = curry(
(schema: Schema.Schema, data: Input<UID.Schema>): Partial<Input<UID.Schema>> => {
const { attributes } = schema;
const componentAttributes = Object.keys(attributes).filter((attributeName) =>
contentTypesUtils.isComponentAttribute(attributes[attributeName])
);

return omit(componentAttributes, data);
}
return omit(componentAttributes, data);
}
);

// NOTE: we could generalize the logic to allow CRUD of relation directly in the DB layer
const createComponents = async <
TUID extends UID.Schema,
TData extends Modules.EntityService.Params.Data.Input<TUID>,
>(
const createComponents = async <TUID extends UID.Schema, TData extends Input<TUID>>(
uid: TUID,
data: TData
) => {
Expand Down Expand Up @@ -80,7 +65,6 @@ const createComponents = async <
throw new Error('Expected an array to create repeatable component');
}

// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
const components: RepeatableComponentValue = await async.map(componentValue, (value: any) =>
createComponent(componentUID, value)
);
Expand All @@ -97,7 +81,7 @@ const createComponents = async <
} else {
const component = await createComponent(
componentUID,
componentValue as Modules.EntityService.Params.Data.Input<UID.Component>
componentValue as Input<UID.Component>
);

componentBody[attributeName] = {
Expand Down Expand Up @@ -164,10 +148,7 @@ const getComponents = async <TUID extends UID.Schema>(
delete old components
create or update
*/
const updateComponents = async <
TUID extends UID.Schema,
TData extends Partial<Modules.EntityService.Params.Data.Input<TUID>>,
>(
const updateComponents = async <TUID extends UID.Schema, TData extends Partial<Input<TUID>>>(
uid: TUID,
entityToUpdate: { id: Modules.EntityService.Params.Attribute.ID },
data: TData
Expand Down Expand Up @@ -356,6 +337,7 @@ const deleteComponents = async <TUID extends UID.Schema, TEntity extends Data.En

if (attribute.type === 'component' || attribute.type === 'dynamiczone') {
let value;

if (loadComponents) {
value = await strapi.db.query(uid).load(entityToDelete, attributeName);
} else {
Expand All @@ -368,13 +350,10 @@ const deleteComponents = async <TUID extends UID.Schema, TEntity extends Data.En

if (attribute.type === 'component') {
const { component: componentUID } = attribute;
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await async.map(_.castArray(value), (subValue: any) =>
deleteComponent(componentUID, subValue)
);
} else {
// delete dynamic zone components
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await async.map(_.castArray(value), (subValue: any) =>
deleteComponent(subValue.__component, subValue)
);
Expand All @@ -390,20 +369,15 @@ const deleteComponents = async <TUID extends UID.Schema, TEntity extends Data.En
************************** */

// components can have nested compos so this must be recursive
const createComponent = async <TUID extends UID.Component>(
uid: TUID,
data: Modules.EntityService.Params.Data.Input<TUID>
) => {
const model = strapi.getModel(uid);
const createComponent = async <TUID extends UID.Component>(uid: TUID, data: Input<TUID>) => {
const schema = strapi.getModel(uid);

const componentData = await createComponents(uid, data);

const transform = pipe(
// Make sure we don't save the component with a pre-defined ID
omit('id'),
// Remove the component data from the original data object ...
(payload) => omitComponentData(model, payload),
// ... and assign the newly created component instead
assign(componentData)
assignComponentData(schema, componentData)
);

return strapi.db.query(uid).create({ data: transform(data) });
Expand All @@ -413,23 +387,23 @@ const createComponent = async <TUID extends UID.Component>(
const updateComponent = async <TUID extends UID.Component>(
uid: TUID,
componentToUpdate: { id: Modules.EntityService.Params.Attribute.ID },
data: Modules.EntityService.Params.Data.Input<TUID>
data: Input<TUID>
) => {
const model = strapi.getModel(uid);
const schema = strapi.getModel(uid);

const componentData = await updateComponents(uid, componentToUpdate, data);

return strapi.db.query(uid).update({
where: {
id: componentToUpdate.id,
},
data: Object.assign(omitComponentData(model, data), componentData),
data: assignComponentData(schema, componentData, data),
});
};

const updateOrCreateComponent = <TUID extends UID.Component>(
componentUID: TUID,
value: Modules.EntityService.Params.Data.Input<TUID>
value: Input<TUID>
) => {
if (value === null) {
return null;
Expand All @@ -453,17 +427,11 @@ const deleteComponent = async <TUID extends UID.Component>(
await strapi.db.query(uid).delete({ where: { id: componentToDelete.id } });
};

const assignComponentData = <TUID extends UID.ContentType>(
data: Modules.EntityService.Params.Data.Input<TUID>,
componentData: ComponentBody,
{
contentType,
}: {
contentType: Schema.ContentType<TUID>;
const assignComponentData = curry(
(schema: Schema.Schema, componentData: ComponentBody, data: Input<UID.Schema>) => {
return pipe(omitComponentData(schema), assign(componentData))(data);
}
) => {
return Object.assign(omitComponentData(contentType, data), componentData);
};
);

export {
omitComponentData,
Expand Down
49 changes: 43 additions & 6 deletions packages/core/core/src/services/document-service/entries.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { UID } from '@strapi/types';
import { async } from '@strapi/utils';
import { assoc, omit } from 'lodash/fp';

import * as components from './components';

import { transformParamsDocumentId } from './transform/id-transform';
import { transformParamsToQuery } from './transform/query';
import { pickSelectionParams } from './params';
import { applyTransforms } from './attributes';
import { transformData } from './transform/data';
import entityValidator from '../entity-validator';

const createEntriesService = (uid: UID.ContentType) => {
Expand All @@ -29,10 +32,13 @@ const createEntriesService = (uid: UID.ContentType) => {

// Component handling
const componentData = await components.createComponents(uid, validData);
const dataWithComponents = components.assignComponentData(validData, componentData, {
const dataWithComponents = components.assignComponentData(
contentType,
});
const entryData = applyTransforms(dataWithComponents, { contentType });
componentData,
validData
);

const entryData = applyTransforms(contentType, dataWithComponents);

const doc = await strapi.db.query(uid).create({ ...query, data: entryData });

Expand Down Expand Up @@ -62,20 +68,51 @@ const createEntriesService = (uid: UID.ContentType) => {
);
// Component handling
const componentData = await components.updateComponents(uid, entryToUpdate, validData as any);
const dataWithComponents = components.assignComponentData(validData, componentData, {
const dataWithComponents = components.assignComponentData(
contentType,
});
const entryData = applyTransforms(dataWithComponents, { contentType });
componentData,
validData
);

const entryData = applyTransforms(contentType, dataWithComponents);

return strapi.db
.query(uid)
.update({ ...query, where: { id: entryToUpdate.id }, data: entryData });
}

async function publishEntry(entry: any, params = {} as any) {
return async.pipe(
omit('id'),
assoc('publishedAt', new Date()),
(draft) => {
const opts = { uid, locale: draft.locale, status: 'published', allowMissingId: true };
return transformData(draft, opts);
},
// Create the published entry
(draft) => createEntry({ ...params, data: draft, locale: draft.locale, status: 'published' })
)(entry);
}

async function discardDraftEntry(entry: any, params = {} as any) {
return async.pipe(
omit('id'),
assoc('publishedAt', null),
(entry) => {
const opts = { uid, locale: entry.locale, status: 'draft', allowMissingId: true };
return transformData(entry, opts);
},
// Create the draft entry
(data) => createEntry({ ...params, locale: data.locale, data, status: 'draft' })
)(entry);
}

return {
create: createEntry,
delete: deleteEntry,
update: updateEntry,
publish: publishEntry,
discardDraft: discardDraftEntry,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const defaultLocale: AsyncTransform = async (contentType, params) => {
}

if (!params.locale) {
// TODO: Load default locale from db in i18n
return assoc('locale', await getDefaultLocale(), params);
}

Expand Down
40 changes: 6 additions & 34 deletions packages/core/core/src/services/document-service/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { createEntriesService } from './entries';
import { pickSelectionParams } from './params';
import { createDocumentId } from '../../utils/transform-content-types-to-models';
import { getDeepPopulate } from './utils/populate';
import { transformData } from './transform/data';
import { transformParamsToQuery } from './transform/query';
import { transformParamsDocumentId } from './transform/id-transform';

Expand Down Expand Up @@ -224,7 +223,7 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
});

// Get deep populate
const entriesToPublish = await strapi.db?.query(uid).findMany({
const draftsToPublish = await strapi.db?.query(uid).findMany({
where: {
...queryParams?.lookup,
documentId,
Expand All @@ -234,26 +233,11 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
});

// Transform draft entry data and create published versions
const publishedEntries = await async.map(
entriesToPublish,
async.pipe(
// Updated at value is used to know if draft has been modified
// If both versions share the same value, it means the draft has not been modified
(draft) => assoc('updatedAt', draft.updatedAt, draft),
assoc('publishedAt', new Date()),
assoc('documentId', documentId),
omit('id'),
// Transform relations to target published versions
(entry) => {
const opts = { uid, locale: entry.locale, status: 'published', allowMissingId: true };
return transformData(entry, opts);
},
// Create the published entry
(data) => entries.create({ ...queryParams, data, locale: data.locale, status: 'published' })
)
const versions = await async.map(draftsToPublish, (draft: unknown) =>
entries.publish(draft, queryParams)
);

return { versions: publishedEntries };
return { versions };
}

async function unpublish(opts = {} as any) {
Expand Down Expand Up @@ -299,20 +283,8 @@ export const createContentTypeRepository: RepositoryFactoryMethod = (uid) => {
});

// Transform published entry data and create draft versions
const draftEntries = await async.map(
entriesToDraft,
async.pipe(
assoc('publishedAt', null),
assoc('documentId', documentId),
omit('id'),
// Transform relations to target draft versions
(entry) => {
const opts = { uid, locale: entry.locale, status: 'draft', allowMissingId: true };
return transformData(entry, opts);
},
// Create the draft entry
(data) => entries.create({ ...queryParams, locale: data.locale, data, status: 'draft' })
)
const draftEntries = await async.map(entriesToDraft, (entry: any) =>
entries.discardDraft(entry, queryParams)
);

return { versions: draftEntries };
Expand Down

0 comments on commit 9535601

Please sign in to comment.