Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Continue extending entries code #20060

Merged
merged 1 commit into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
alexandrebodin marked this conversation as resolved.
Show resolved Hide resolved
};
};

Expand Down
19 changes: 9 additions & 10 deletions packages/core/core/src/services/document-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,17 @@ export const createDocumentService = (strapi: Core.Strapi): Modules.Documents.Se
const contentType = strapi.contentType(uid);
const repository = createContentTypeRepository(uid);

repositories.set(
uid,
middlewares.wrapObject(
repository,
{ uid, contentType },
{
exclude: ['updateComponents', 'omitComponentData'],
}
)
const instance = middlewares.wrapObject(
repository,
{ uid, contentType },
{
exclude: ['updateComponents', 'omitComponentData'],
}
);

return repository;
repositories.set(uid, instance);

return instance;
} as Modules.Documents.Service;

return Object.assign(factory, {
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Middleware = (ctx: any, next: () => Promise<void>) => Promise<void>;
export type Middleware = (ctx: any, next: () => Promise<void>) => Promise<void> | void;

export type Options = {
exclude?: string[];
Expand Down Expand Up @@ -39,7 +39,7 @@ export const createMiddlewareManager = () => {
if (exclude.includes(key)) {
facade[key] = prop;
} else if (typeof prop === 'function') {
const newMethod = async (params: any) => {
const newMethod = async (params: any = {}) => {
const ctx = {
...ctxDefaults,
action: key,
Expand Down