mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
chore: rename doc service
This commit is contained in:
parent
cb1114d267
commit
83318fb940
@ -65,7 +65,7 @@ import getNumberOfDynamicZones from './services/utils/dynamic-zones';
|
||||
import convertCustomFieldType from './utils/convert-custom-field-type';
|
||||
import { transformContentTypesToModels } from './utils/transform-content-types-to-models';
|
||||
import { FeaturesService, createFeaturesService } from './services/features';
|
||||
import { createDocumentEngine } from './services/document-service/document-engine';
|
||||
import { createDocumentService } from './services/document-service/document-service';
|
||||
|
||||
/**
|
||||
* Resolve the working directories based on the instance options.
|
||||
@ -152,7 +152,7 @@ class Strapi extends Container implements StrapiI {
|
||||
|
||||
entityService?: EntityService.EntityService;
|
||||
|
||||
documents?: Documents.Repository;
|
||||
documents?: Documents.Service;
|
||||
|
||||
telemetry: TelemetryService;
|
||||
|
||||
@ -512,7 +512,7 @@ class Strapi extends Container implements StrapiI {
|
||||
entityValidator: this.entityValidator,
|
||||
});
|
||||
|
||||
this.documents = createDocumentEngine(this);
|
||||
this.documents = createDocumentService(this);
|
||||
|
||||
if (this.config.get('server.cron.enabled', true)) {
|
||||
const cronTasks = this.config.get('server.cron.tasks', {});
|
||||
|
@ -1,159 +1,300 @@
|
||||
import { Strapi, Common, Documents } from '@strapi/types';
|
||||
import createDocumentRepository from './document-repository';
|
||||
import createMiddlewareManager from './middlewares';
|
||||
import { loadDefaultMiddlewares } from './middlewares/defaults';
|
||||
import type { Database } from '@strapi/database';
|
||||
import type { Common, Documents, Schema, Shared, Strapi } from '@strapi/types';
|
||||
import { contentTypes as contentTypesUtils, convertQueryParams, mapAsync } from '@strapi/utils';
|
||||
|
||||
import { isArray, omit } from 'lodash/fp';
|
||||
import uploadFiles from '../utils/upload-files';
|
||||
|
||||
import {
|
||||
cloneComponents,
|
||||
createComponents,
|
||||
deleteComponents,
|
||||
getComponents,
|
||||
omitComponentData,
|
||||
updateComponents,
|
||||
} from '../entity-service/components';
|
||||
|
||||
import { createDocumentId } from '../../utils/transform-content-types-to-models';
|
||||
import { applyTransforms } from '../entity-service/attributes';
|
||||
import entityValidator from '../entity-validator';
|
||||
import { pickSelectionParams } from './params';
|
||||
|
||||
const { transformParamsToQuery } = convertQueryParams;
|
||||
|
||||
/**
|
||||
* Repository to :
|
||||
* - Access documents via actions (findMany, findOne, create, update, delete, ...)
|
||||
* - Execute middlewares on document actions
|
||||
* - Apply default parameters to document actions
|
||||
*
|
||||
* @param strapi
|
||||
* @param options.defaults - Default parameters to apply to all actions
|
||||
* @param options.parent - Parent repository, used when creating a new repository with .with()
|
||||
* @returns DocumentRepository
|
||||
*
|
||||
* @example Access documents
|
||||
* const article = strapi.documents('api::article.article').create(params)
|
||||
* const allArticles = strapi.documents('api::article.article').findMany(params)
|
||||
* TODO: Sanitization / validation built-in
|
||||
* TODO: i18n - Move logic to i18n package
|
||||
* TODO: Webhooks
|
||||
* TODO: Audit logs
|
||||
* TODO: Entity Validation - Uniqueness across same locale and publication status
|
||||
* TODO: File upload
|
||||
* TODO: replace 'any'
|
||||
* TODO: availableLocales
|
||||
*
|
||||
*/
|
||||
export const createDocumentEngine = (
|
||||
strapi: Strapi,
|
||||
{ defaults = {} }: { defaults?: any } = {}
|
||||
): Documents.Engine => {
|
||||
const documents = createDocumentRepository({ strapi, db: strapi.db! });
|
||||
|
||||
const middlewareManager = createMiddlewareManager();
|
||||
loadDefaultMiddlewares(middlewareManager);
|
||||
|
||||
function create<TContentTypeUID extends Common.UID.ContentType>(
|
||||
uid: TContentTypeUID
|
||||
): Documents.EngineInstance<TContentTypeUID> {
|
||||
return {
|
||||
async findMany(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findMany', uid, params, options: {} }, ({ params }) =>
|
||||
documents.findMany(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async findFirst(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findFirst', uid, params, options: {} }, ({ params }) =>
|
||||
documents.findFirst(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async findOne(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findOne', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.findOne(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async delete(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'delete', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.delete(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async deleteMany(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'deleteMany', uid, params, options: {} }, ({ params }) =>
|
||||
documents.deleteMany(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async create(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'create', uid, params, options: {} }, ({ params }) =>
|
||||
documents.create(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async clone(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'clone', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.clone(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async update(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'update', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.update(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async count(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'count', uid, params, options: {} }, ({ params }) =>
|
||||
documents.count(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async publish(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'publish', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.publish(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async unpublish(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run(
|
||||
{ action: 'unpublish', uid, params, options: { id } },
|
||||
({ params }) => documents.unpublish(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async discardDraft(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run(
|
||||
{ action: 'discardDraft', uid, params, options: { id } },
|
||||
({ params }) => documents.discardDraft(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
// @ts-expect-error - TODO: Fix this
|
||||
with(params: object) {
|
||||
return createDocumentEngine(strapi, {
|
||||
defaults: { ...defaults, ...params },
|
||||
})(uid);
|
||||
},
|
||||
|
||||
use(action, cb, opts) {
|
||||
middlewareManager.add(uid, action, cb, opts);
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(create, {
|
||||
use(action: any, cb: any, opts?: any) {
|
||||
middlewareManager.add('_all', action, cb, opts);
|
||||
return create;
|
||||
},
|
||||
middlewares: middlewareManager,
|
||||
// NOTE : We should do this in a different way, where lifecycles are executed for the different methods
|
||||
...documents,
|
||||
});
|
||||
|
||||
// @ts-expect-error - TODO: Fix this
|
||||
return create;
|
||||
type Context = {
|
||||
contentType: Schema.ContentType;
|
||||
};
|
||||
|
||||
const createPipeline = (data: Record<string, unknown>, context: Context) => {
|
||||
return applyTransforms(data, context);
|
||||
};
|
||||
|
||||
const updatePipeline = (data: Record<string, unknown>, context: Context) => {
|
||||
return applyTransforms(data, context);
|
||||
};
|
||||
|
||||
const createDocumentEngine = ({
|
||||
strapi,
|
||||
db,
|
||||
}: {
|
||||
strapi: Strapi;
|
||||
db: Database;
|
||||
}): Documents.Engine => ({
|
||||
uploadFiles,
|
||||
|
||||
async findMany(uid, params) {
|
||||
const { kind } = strapi.getModel(uid);
|
||||
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
query.where = { ...params?.lookup, ...query.where };
|
||||
|
||||
if (kind === 'singleType') {
|
||||
return db.query(uid).findOne(query);
|
||||
}
|
||||
|
||||
return db.query(uid).findMany(query);
|
||||
},
|
||||
|
||||
async findFirst(uid, params) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
|
||||
return db.query(uid).findOne({ ...query, where: { ...params?.lookup, ...query.where } });
|
||||
},
|
||||
|
||||
async findOne(uid, documentId, params) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
return db
|
||||
.query(uid)
|
||||
.findOne({ ...query, where: { ...params?.lookup, ...query.where, documentId } });
|
||||
},
|
||||
|
||||
async delete(uid, documentId, params = {} as any) {
|
||||
const query = transformParamsToQuery(uid, params as any);
|
||||
|
||||
if (params.status === 'draft') {
|
||||
throw new Error('Cannot delete a draft document');
|
||||
}
|
||||
|
||||
const entriesToDelete = await db.query(uid).findMany({
|
||||
...query,
|
||||
where: {
|
||||
...params.lookup,
|
||||
...query?.where,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
// Delete all matched entries and its components
|
||||
await mapAsync(entriesToDelete, async (entryToDelete: any) => {
|
||||
const componentsToDelete = await getComponents(uid, entryToDelete);
|
||||
await db.query(uid).delete({ where: { id: entryToDelete.id } });
|
||||
await deleteComponents(uid, componentsToDelete as any, { loadComponents: false });
|
||||
});
|
||||
|
||||
// TODO: Change return value to actual count
|
||||
return { versions: entriesToDelete };
|
||||
},
|
||||
|
||||
// TODO: should we provide two separate methods?
|
||||
async deleteMany(uid, paramsOrIds) {
|
||||
let queryParams;
|
||||
if (isArray(paramsOrIds)) {
|
||||
queryParams = { filter: { documentID: { $in: paramsOrIds } } };
|
||||
} else {
|
||||
queryParams = paramsOrIds;
|
||||
}
|
||||
|
||||
const query = transformParamsToQuery(uid, queryParams || ({} as any));
|
||||
|
||||
return db.query(uid).deleteMany(query);
|
||||
},
|
||||
|
||||
async create(uid, params) {
|
||||
// TODO: Entity validator.
|
||||
// TODO: File upload - Probably in the lifecycles?
|
||||
const { data } = params;
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Create requires data attribute');
|
||||
}
|
||||
|
||||
const model = strapi.getModel(uid) as Shared.ContentTypes[Common.UID.ContentType];
|
||||
|
||||
const validData = await entityValidator.validateEntityCreation(model, data, { isDraft: true });
|
||||
|
||||
const componentData = await createComponents(uid, validData);
|
||||
const entryData = createPipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{
|
||||
contentType: model,
|
||||
}
|
||||
);
|
||||
|
||||
// select / populate
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params) as any);
|
||||
|
||||
return db.query(uid).create({ ...query, data: entryData });
|
||||
},
|
||||
|
||||
// NOTE: What happens if user doesn't provide specific publications state and locale to update?
|
||||
async update(uid, documentId, params) {
|
||||
// TODO: Prevent updating a published document
|
||||
// TODO: Entity validator.
|
||||
// TODO: File upload
|
||||
const { data } = params || {};
|
||||
const model = strapi.getModel(uid) as Shared.ContentTypes[Common.UID.ContentType];
|
||||
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params || {}) as any);
|
||||
|
||||
// Find all locales of the document
|
||||
const entryToUpdate = await db
|
||||
.query(uid)
|
||||
.findOne({ ...query, where: { ...params?.lookup, ...query?.where, documentId } });
|
||||
|
||||
// Document does not exist
|
||||
if (!entryToUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validData = await entityValidator.validateEntityUpdate(
|
||||
model,
|
||||
data,
|
||||
{ isDraft: true }, // Always update the draft version
|
||||
entryToUpdate
|
||||
);
|
||||
|
||||
const componentData = await updateComponents(uid, entryToUpdate, validData);
|
||||
const entryData = updatePipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{ contentType: model }
|
||||
);
|
||||
|
||||
return db.query(uid).update({ ...query, where: { id: entryToUpdate.id }, data: entryData });
|
||||
},
|
||||
|
||||
async count(uid, params = undefined) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
query.where = { ...params?.lookup, ...query.where };
|
||||
|
||||
return db.query(uid).count(query);
|
||||
},
|
||||
|
||||
async clone(uid, documentId, params) {
|
||||
// TODO: File upload
|
||||
// TODO: Entity validator.
|
||||
const { data = {} as any } = params!;
|
||||
|
||||
const model = strapi.getModel(uid);
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params) as any);
|
||||
|
||||
// Find all locales of the document
|
||||
const entries = await db.query(uid).findMany({
|
||||
...query,
|
||||
where: { ...params?.lookup, ...query.where, documentId },
|
||||
});
|
||||
|
||||
// Document does not exist
|
||||
if (!entries.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newDocumentId = createDocumentId();
|
||||
|
||||
const versions = await mapAsync(entries, async (entryToClone: any) => {
|
||||
const isDraft = contentTypesUtils.isDraft(data);
|
||||
// Todo: Merge data with entry to clone
|
||||
const validData = await entityValidator.validateEntityUpdate(
|
||||
model,
|
||||
// Omit id fields, the cloned entity id will be generated by the database
|
||||
omit(['id'], data),
|
||||
{ isDraft },
|
||||
entryToClone
|
||||
);
|
||||
|
||||
const componentData = await cloneComponents(uid, entryToClone, validData);
|
||||
const entityData = createPipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{ contentType: model }
|
||||
);
|
||||
|
||||
// TODO: Transform params to query
|
||||
return db.query(uid).clone(entryToClone.id, {
|
||||
...query,
|
||||
// Allows entityData to override the documentId (e.g. when publishing)
|
||||
data: { documentId: newDocumentId, ...entityData, locale: entryToClone.locale },
|
||||
});
|
||||
});
|
||||
|
||||
return { id: newDocumentId, versions };
|
||||
},
|
||||
|
||||
// TODO: Handle relations so they target the published version
|
||||
async publish(uid, documentId, params) {
|
||||
// Delete already published versions that match the locales to be published
|
||||
await this.delete(uid, documentId, {
|
||||
...params,
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
});
|
||||
|
||||
// Clone every draft version to be published
|
||||
const clonedDocuments = (await this.clone(uid, documentId, {
|
||||
...(params || {}),
|
||||
// @ts-expect-error - Generic type does not have publishedAt attribute by default
|
||||
data: { documentId, publishedAt: new Date() },
|
||||
})) as any;
|
||||
|
||||
// TODO: Return actual count
|
||||
return { versions: clonedDocuments?.versions || [] };
|
||||
},
|
||||
|
||||
async unpublish(uid, documentId, params) {
|
||||
// Delete all published versions
|
||||
return this.delete(uid, documentId, {
|
||||
...params,
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
}) as any;
|
||||
},
|
||||
|
||||
/**
|
||||
* Steps:
|
||||
* - Delete the matching draft versions (publishedAt = null)
|
||||
* - Clone the matching published versions into draft versions
|
||||
*/
|
||||
async discardDraft(uid, documentId, params) {
|
||||
// Delete draft versions, clone published versions into draft versions
|
||||
await this.delete(uid, documentId, {
|
||||
...params,
|
||||
// Delete all drafts that match query
|
||||
lookup: { ...params?.lookup, publishedAt: null },
|
||||
});
|
||||
|
||||
// Clone published versions into draft versions
|
||||
const clonedDocuments = (await this.clone(uid, documentId, {
|
||||
...(params || {}),
|
||||
// Clone only published versions
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
// @ts-expect-error - Generic type does not have publishedAt attribute by default
|
||||
data: { documentId, publishedAt: null },
|
||||
})) as any;
|
||||
|
||||
return { versions: clonedDocuments?.versions || [] };
|
||||
},
|
||||
});
|
||||
|
||||
export default (ctx: { strapi: Strapi; db: Database }): Documents.Engine => {
|
||||
const implementation = createDocumentEngine(ctx);
|
||||
|
||||
// TODO: Wrap with database error handling
|
||||
return implementation;
|
||||
};
|
||||
|
@ -1,300 +0,0 @@
|
||||
import type { Database } from '@strapi/database';
|
||||
import type { Common, Documents, Schema, Shared, Strapi } from '@strapi/types';
|
||||
import { contentTypes as contentTypesUtils, convertQueryParams, mapAsync } from '@strapi/utils';
|
||||
|
||||
import { isArray, omit } from 'lodash/fp';
|
||||
import uploadFiles from '../utils/upload-files';
|
||||
|
||||
import {
|
||||
cloneComponents,
|
||||
createComponents,
|
||||
deleteComponents,
|
||||
getComponents,
|
||||
omitComponentData,
|
||||
updateComponents,
|
||||
} from '../entity-service/components';
|
||||
|
||||
import { createDocumentId } from '../../utils/transform-content-types-to-models';
|
||||
import { applyTransforms } from '../entity-service/attributes';
|
||||
import entityValidator from '../entity-validator';
|
||||
import { pickSelectionParams } from './params';
|
||||
|
||||
const { transformParamsToQuery } = convertQueryParams;
|
||||
|
||||
/**
|
||||
* TODO: Sanitization / validation built-in
|
||||
* TODO: i18n - Move logic to i18n package
|
||||
* TODO: Webhooks
|
||||
* TODO: Audit logs
|
||||
* TODO: Entity Validation - Uniqueness across same locale and publication status
|
||||
* TODO: File upload
|
||||
* TODO: replace 'any'
|
||||
* TODO: availableLocales
|
||||
*
|
||||
*/
|
||||
type Context = {
|
||||
contentType: Schema.ContentType;
|
||||
};
|
||||
|
||||
const createPipeline = (data: Record<string, unknown>, context: Context) => {
|
||||
return applyTransforms(data, context);
|
||||
};
|
||||
|
||||
const updatePipeline = (data: Record<string, unknown>, context: Context) => {
|
||||
return applyTransforms(data, context);
|
||||
};
|
||||
|
||||
const createDocumentRepository = ({
|
||||
strapi,
|
||||
db,
|
||||
}: {
|
||||
strapi: Strapi;
|
||||
db: Database;
|
||||
}): Documents.Repository => ({
|
||||
uploadFiles,
|
||||
|
||||
async findMany(uid, params) {
|
||||
const { kind } = strapi.getModel(uid);
|
||||
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
query.where = { ...params?.lookup, ...query.where };
|
||||
|
||||
if (kind === 'singleType') {
|
||||
return db.query(uid).findOne(query);
|
||||
}
|
||||
|
||||
return db.query(uid).findMany(query);
|
||||
},
|
||||
|
||||
async findFirst(uid, params) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
|
||||
return db.query(uid).findOne({ ...query, where: { ...params?.lookup, ...query.where } });
|
||||
},
|
||||
|
||||
async findOne(uid, documentId, params) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
return db
|
||||
.query(uid)
|
||||
.findOne({ ...query, where: { ...params?.lookup, ...query.where, documentId } });
|
||||
},
|
||||
|
||||
async delete(uid, documentId, params = {} as any) {
|
||||
const query = transformParamsToQuery(uid, params as any);
|
||||
|
||||
if (params.status === 'draft') {
|
||||
throw new Error('Cannot delete a draft document');
|
||||
}
|
||||
|
||||
const entriesToDelete = await db.query(uid).findMany({
|
||||
...query,
|
||||
where: {
|
||||
...params.lookup,
|
||||
...query?.where,
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
// Delete all matched entries and its components
|
||||
await mapAsync(entriesToDelete, async (entryToDelete: any) => {
|
||||
const componentsToDelete = await getComponents(uid, entryToDelete);
|
||||
await db.query(uid).delete({ where: { id: entryToDelete.id } });
|
||||
await deleteComponents(uid, componentsToDelete as any, { loadComponents: false });
|
||||
});
|
||||
|
||||
// TODO: Change return value to actual count
|
||||
return { versions: entriesToDelete };
|
||||
},
|
||||
|
||||
// TODO: should we provide two separate methods?
|
||||
async deleteMany(uid, paramsOrIds) {
|
||||
let queryParams;
|
||||
if (isArray(paramsOrIds)) {
|
||||
queryParams = { filter: { documentID: { $in: paramsOrIds } } };
|
||||
} else {
|
||||
queryParams = paramsOrIds;
|
||||
}
|
||||
|
||||
const query = transformParamsToQuery(uid, queryParams || ({} as any));
|
||||
|
||||
return db.query(uid).deleteMany(query);
|
||||
},
|
||||
|
||||
async create(uid, params) {
|
||||
// TODO: Entity validator.
|
||||
// TODO: File upload - Probably in the lifecycles?
|
||||
const { data } = params;
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Create requires data attribute');
|
||||
}
|
||||
|
||||
const model = strapi.getModel(uid) as Shared.ContentTypes[Common.UID.ContentType];
|
||||
|
||||
const validData = await entityValidator.validateEntityCreation(model, data, { isDraft: true });
|
||||
|
||||
const componentData = await createComponents(uid, validData);
|
||||
const entryData = createPipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{
|
||||
contentType: model,
|
||||
}
|
||||
);
|
||||
|
||||
// select / populate
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params));
|
||||
|
||||
return db.query(uid).create({ ...query, data: entryData });
|
||||
},
|
||||
|
||||
// NOTE: What happens if user doesn't provide specific publications state and locale to update?
|
||||
async update(uid, documentId, params) {
|
||||
// TODO: Prevent updating a published document
|
||||
// TODO: Entity validator.
|
||||
// TODO: File upload
|
||||
const { data } = params || {};
|
||||
const model = strapi.getModel(uid) as Shared.ContentTypes[Common.UID.ContentType];
|
||||
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params || {}));
|
||||
|
||||
// Find all locales of the document
|
||||
const entryToUpdate = await db
|
||||
.query(uid)
|
||||
.findOne({ ...query, where: { ...params?.lookup, ...query?.where, documentId } });
|
||||
|
||||
// Document does not exist
|
||||
if (!entryToUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validData = await entityValidator.validateEntityUpdate(
|
||||
model,
|
||||
data,
|
||||
{ isDraft: true }, // Always update the draft version
|
||||
entryToUpdate
|
||||
);
|
||||
|
||||
const componentData = await updateComponents(uid, entryToUpdate, validData);
|
||||
const entryData = updatePipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{ contentType: model }
|
||||
);
|
||||
|
||||
return db.query(uid).update({ ...query, where: { id: entryToUpdate.id }, data: entryData });
|
||||
},
|
||||
|
||||
async count(uid, params = undefined) {
|
||||
const query = transformParamsToQuery(uid, params || ({} as any));
|
||||
query.where = { ...params?.lookup, ...query.where };
|
||||
|
||||
return db.query(uid).count(query);
|
||||
},
|
||||
|
||||
async clone(uid, documentId, params) {
|
||||
// TODO: File upload
|
||||
// TODO: Entity validator.
|
||||
const { data = {} as any } = params!;
|
||||
|
||||
const model = strapi.getModel(uid);
|
||||
const query = transformParamsToQuery(uid, pickSelectionParams(params));
|
||||
|
||||
// Find all locales of the document
|
||||
const entries = await db.query(uid).findMany({
|
||||
...query,
|
||||
where: { ...params?.lookup, ...query.where, documentId },
|
||||
});
|
||||
|
||||
// Document does not exist
|
||||
if (!entries.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newDocumentId = createDocumentId();
|
||||
|
||||
const versions = await mapAsync(entries, async (entryToClone: any) => {
|
||||
const isDraft = contentTypesUtils.isDraft(data);
|
||||
// Todo: Merge data with entry to clone
|
||||
const validData = await entityValidator.validateEntityUpdate(
|
||||
model,
|
||||
// Omit id fields, the cloned entity id will be generated by the database
|
||||
omit(['id'], data),
|
||||
{ isDraft },
|
||||
entryToClone
|
||||
);
|
||||
|
||||
const componentData = await cloneComponents(uid, entryToClone, validData);
|
||||
const entityData = createPipeline(
|
||||
Object.assign(omitComponentData(model, validData), componentData),
|
||||
{ contentType: model }
|
||||
);
|
||||
|
||||
// TODO: Transform params to query
|
||||
return db.query(uid).clone(entryToClone.id, {
|
||||
...query,
|
||||
// Allows entityData to override the documentId (e.g. when publishing)
|
||||
data: { documentId: newDocumentId, ...entityData, locale: entryToClone.locale },
|
||||
});
|
||||
});
|
||||
|
||||
return { id: newDocumentId, versions };
|
||||
},
|
||||
|
||||
// TODO: Handle relations so they target the published version
|
||||
async publish(uid, documentId, params) {
|
||||
// Delete already published versions that match the locales to be published
|
||||
await this.delete(uid, documentId, {
|
||||
...params,
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
});
|
||||
|
||||
// Clone every draft version to be published
|
||||
const clonedDocuments = (await this.clone(uid, documentId, {
|
||||
...(params || {}),
|
||||
// @ts-expect-error - Generic type does not have publishedAt attribute by default
|
||||
data: { documentId, publishedAt: new Date() },
|
||||
})) as any;
|
||||
|
||||
// TODO: Return actual count
|
||||
return { versions: clonedDocuments?.versions || [] };
|
||||
},
|
||||
|
||||
async unpublish(uid, documentId, params) {
|
||||
// Delete all published versions
|
||||
return this.delete(uid, documentId, {
|
||||
...params,
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
}) as any;
|
||||
},
|
||||
|
||||
/**
|
||||
* Steps:
|
||||
* - Delete the matching draft versions (publishedAt = null)
|
||||
* - Clone the matching published versions into draft versions
|
||||
*/
|
||||
async discardDraft(uid, documentId, params) {
|
||||
// Delete draft versions, clone published versions into draft versions
|
||||
await this.delete(uid, documentId, {
|
||||
...params,
|
||||
// Delete all drafts that match query
|
||||
lookup: { ...params?.lookup, publishedAt: null },
|
||||
});
|
||||
|
||||
// Clone published versions into draft versions
|
||||
const clonedDocuments = (await this.clone(uid, documentId, {
|
||||
...(params || {}),
|
||||
// Clone only published versions
|
||||
lookup: { ...params?.lookup, publishedAt: { $ne: null } },
|
||||
// @ts-expect-error - Generic type does not have publishedAt attribute by default
|
||||
data: { documentId, publishedAt: null },
|
||||
})) as any;
|
||||
|
||||
return { versions: clonedDocuments?.versions || [] };
|
||||
},
|
||||
});
|
||||
|
||||
export default (ctx: { strapi: Strapi; db: Database }): Documents.Repository => {
|
||||
const implementation = createDocumentRepository(ctx);
|
||||
|
||||
// TODO: Wrap with database error handling
|
||||
return implementation;
|
||||
};
|
@ -0,0 +1,159 @@
|
||||
import { Strapi, Common, Documents } from '@strapi/types';
|
||||
import createDocumentRepository from './document-engine';
|
||||
import createMiddlewareManager from './middlewares';
|
||||
import { loadDefaultMiddlewares } from './middlewares/defaults';
|
||||
|
||||
/**
|
||||
* Repository to :
|
||||
* - Access documents via actions (findMany, findOne, create, update, delete, ...)
|
||||
* - Execute middlewares on document actions
|
||||
* - Apply default parameters to document actions
|
||||
*
|
||||
* @param strapi
|
||||
* @param options.defaults - Default parameters to apply to all actions
|
||||
* @param options.parent - Parent repository, used when creating a new repository with .with()
|
||||
* @returns DocumentService
|
||||
*
|
||||
* @example Access documents
|
||||
* const article = strapi.documents('api::article.article').create(params)
|
||||
* const allArticles = strapi.documents('api::article.article').findMany(params)
|
||||
*
|
||||
*/
|
||||
export const createDocumentService = (
|
||||
strapi: Strapi,
|
||||
{ defaults = {} }: { defaults?: any } = {}
|
||||
): Documents.Service => {
|
||||
const documents = createDocumentRepository({ strapi, db: strapi.db! });
|
||||
|
||||
const middlewareManager = createMiddlewareManager();
|
||||
loadDefaultMiddlewares(middlewareManager);
|
||||
|
||||
function create<TContentTypeUID extends Common.UID.ContentType>(
|
||||
uid: TContentTypeUID
|
||||
): Documents.ServiceInstance<TContentTypeUID> {
|
||||
return {
|
||||
async findMany(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findMany', uid, params, options: {} }, ({ params }) =>
|
||||
documents.findMany(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async findFirst(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findFirst', uid, params, options: {} }, ({ params }) =>
|
||||
documents.findFirst(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async findOne(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'findOne', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.findOne(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async delete(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'delete', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.delete(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async deleteMany(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'deleteMany', uid, params, options: {} }, ({ params }) =>
|
||||
documents.deleteMany(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async create(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'create', uid, params, options: {} }, ({ params }) =>
|
||||
documents.create(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async clone(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'clone', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.clone(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async update(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'update', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.update(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async count(params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'count', uid, params, options: {} }, ({ params }) =>
|
||||
documents.count(uid, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async publish(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run({ action: 'publish', uid, params, options: { id } }, ({ params }) =>
|
||||
documents.publish(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async unpublish(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run(
|
||||
{ action: 'unpublish', uid, params, options: { id } },
|
||||
({ params }) => documents.unpublish(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async discardDraft(id: string, params = {} as any) {
|
||||
return strapi.db?.transaction?.(async () =>
|
||||
middlewareManager.run(
|
||||
{ action: 'discardDraft', uid, params, options: { id } },
|
||||
({ params }) => documents.discardDraft(uid, id, params)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
// @ts-expect-error - TODO: Fix this
|
||||
with(params: object) {
|
||||
return createDocumentService(strapi, {
|
||||
defaults: { ...defaults, ...params },
|
||||
})(uid);
|
||||
},
|
||||
|
||||
use(action, cb, opts) {
|
||||
middlewareManager.add(uid, action, cb, opts);
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(create, {
|
||||
use(action: any, cb: any, opts?: any) {
|
||||
middlewareManager.add('_all', action, cb, opts);
|
||||
return create;
|
||||
},
|
||||
middlewares: middlewareManager,
|
||||
// NOTE : We should do this in a different way, where lifecycles are executed for the different methods
|
||||
...documents,
|
||||
});
|
||||
|
||||
// @ts-expect-error - TODO: Fix this
|
||||
return create;
|
||||
};
|
@ -108,7 +108,7 @@ export interface Strapi extends Container {
|
||||
store?: CoreStore;
|
||||
entityValidator?: EntityValidator;
|
||||
entityService?: EntityService.EntityService;
|
||||
documents?: Documents.Engine;
|
||||
documents?: Documents.Service;
|
||||
telemetry: TelemetryService;
|
||||
requestContext: RequestContext;
|
||||
customFields: CustomFields.CustomFields;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Common } from '../../types';
|
||||
import type * as Params from './params/document-repository';
|
||||
import type * as Result from './result/document-repository';
|
||||
import type * as Params from './params/document-engine';
|
||||
import type * as Result from './result/document-enigne';
|
||||
|
||||
export type ID = string;
|
||||
|
||||
@ -10,7 +10,7 @@ export type UploadFile = (
|
||||
files: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
|
||||
export interface DocumentRepository {
|
||||
export interface DocumentEngine {
|
||||
uploadFiles: UploadFile;
|
||||
|
||||
findMany<
|
@ -1,16 +1,16 @@
|
||||
import { Common } from '../..';
|
||||
import { ID, type DocumentRepository } from './document-repository';
|
||||
import { ID, type DocumentEngine } from './document-engine';
|
||||
import type * as Middleware from './middleware';
|
||||
import type * as Params from './params/document-repository';
|
||||
import type * as Result from './result/document-repository';
|
||||
import type * as Params from './params/document-engine';
|
||||
import type * as Result from './result/document-enigne';
|
||||
|
||||
export { ID, DocumentRepository as Repository } from './document-repository';
|
||||
export { ID, DocumentEngine as Engine } from './document-engine';
|
||||
export type * as Middleware from './middleware';
|
||||
export * as Params from './params';
|
||||
export * from './plugin';
|
||||
export * from './result';
|
||||
|
||||
export type EngineInstance<
|
||||
export type ServiceInstance<
|
||||
TContentTypeUID extends Common.UID.ContentType = Common.UID.ContentType
|
||||
> = {
|
||||
findMany: <TParams extends Params.FindMany<TContentTypeUID>>(
|
||||
@ -74,7 +74,7 @@ export type EngineInstance<
|
||||
* return result;
|
||||
* })
|
||||
*/
|
||||
use: <TAction extends keyof DocumentRepository>(
|
||||
use: <TAction extends keyof DocumentEngine>(
|
||||
action: TAction,
|
||||
// QUESTION: How do we type the result type of next?
|
||||
// Should we send params + document id attribute?
|
||||
@ -82,7 +82,7 @@ export type EngineInstance<
|
||||
| Middleware.Middleware<Common.UID.ContentType, TAction>
|
||||
| Middleware.Middleware<Common.UID.ContentType, TAction>[],
|
||||
opts?: Middleware.Options
|
||||
) => ThisType<EngineInstance<TContentTypeUID>>;
|
||||
) => ThisType<ServiceInstance<TContentTypeUID>>;
|
||||
|
||||
/**
|
||||
* `.with()` instantiates a new document repository with default parameters
|
||||
@ -99,13 +99,13 @@ export type EngineInstance<
|
||||
*/
|
||||
with: <TParams extends Params.With<TContentTypeUID>>(
|
||||
params?: TParams
|
||||
) => EngineInstance<TContentTypeUID>;
|
||||
) => ServiceInstance<TContentTypeUID>;
|
||||
};
|
||||
|
||||
export type Engine = {
|
||||
export type Service = {
|
||||
<TContentTypeUID extends Common.UID.ContentType>(
|
||||
uid: TContentTypeUID
|
||||
): EngineInstance<TContentTypeUID>;
|
||||
): ServiceInstance<TContentTypeUID>;
|
||||
|
||||
/** Add a middleware for all uid's and a specific action
|
||||
* @example - Add a default locale
|
||||
@ -114,13 +114,13 @@ export type Engine = {
|
||||
* return next(ctx)
|
||||
* })
|
||||
*/
|
||||
use: <TAction extends keyof DocumentRepository>(
|
||||
use: <TAction extends keyof DocumentEngine>(
|
||||
action: TAction,
|
||||
cb:
|
||||
| Middleware.Middleware<Common.UID.ContentType, TAction>
|
||||
| Middleware.Middleware<Common.UID.ContentType, TAction>[],
|
||||
opts?: Middleware.Options
|
||||
) => Engine;
|
||||
) => Service;
|
||||
|
||||
middlewares: Middleware.Manager;
|
||||
} & DocumentRepository;
|
||||
} & DocumentEngine;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Utility type to reuse Param definition in MiddlewareContext
|
||||
import { Common } from '../..';
|
||||
import { DocumentRepository } from './document-repository';
|
||||
import type * as Params from './params/document-repository';
|
||||
import { DocumentEngine } from './document-engine';
|
||||
import type * as Params from './params/document-engine';
|
||||
|
||||
export type ParamsMap<TContentTypeUID extends Common.UID.ContentType = Common.UID.ContentType> = {
|
||||
findOne: Params.FindOne<TContentTypeUID>;
|
||||
@ -20,7 +20,7 @@ export type ParamsMap<TContentTypeUID extends Common.UID.ContentType = Common.UI
|
||||
|
||||
export interface Context<
|
||||
TContentTypeUID extends Common.UID.ContentType = Common.UID.ContentType,
|
||||
TAction extends keyof DocumentRepository = keyof DocumentRepository
|
||||
TAction extends keyof DocumentEngine = keyof DocumentEngine
|
||||
> {
|
||||
uid: TContentTypeUID;
|
||||
action: TAction;
|
||||
@ -40,11 +40,11 @@ export interface Options {
|
||||
|
||||
export type Middleware<
|
||||
TContentTypeUID extends Common.UID.ContentType,
|
||||
TAction extends keyof DocumentRepository
|
||||
TAction extends keyof DocumentEngine
|
||||
> = (
|
||||
ctx: Context<TContentTypeUID, TAction>,
|
||||
next: (ctx: Context<TContentTypeUID, TAction>) => ReturnType<DocumentRepository[TAction]>
|
||||
) => ReturnType<DocumentRepository[TAction]>;
|
||||
next: (ctx: Context<TContentTypeUID, TAction>) => ReturnType<DocumentEngine[TAction]>
|
||||
) => ReturnType<DocumentEngine[TAction]>;
|
||||
|
||||
/**
|
||||
* Handles middlewares for document service
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Common, Utils } from '../../..';
|
||||
import { Result } from '.';
|
||||
import * as Params from '../params/document-repository';
|
||||
import * as Params from '../params/document-engine';
|
||||
|
||||
export type CountResult = { count: number };
|
||||
|
Loading…
x
Reference in New Issue
Block a user