Implement compos in the entity service

This commit is contained in:
Alexandre Bodin
2021-07-06 14:18:03 +02:00
parent 683fe18606
commit 09f7269b4a
15 changed files with 297 additions and 72 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6"
},
"include": ["packages/**/*"],
"exclude": ["node_modules", "**/node_modules/*"]
}
@@ -27,15 +27,15 @@ describe('Content Type Builder - Content types', () => {
});
afterAll(async () => {
const modelsName = [
'test-collection-type',
'test-collection',
'test-single-type',
'ct-with-dp',
const modelsUIDs = [
'application::test-collection-type.test-collection-type',
'application::test-collection.test-collection',
'application::test-single-type.test-single-type',
'application::ct-with-dp.ct-with-dp',
];
await modelsUtils.cleanupModels(modelsName, { strapi });
await modelsUtils.deleteContentTypes(modelsName, { strapi });
await modelsUtils.cleanupModels(modelsUIDs, { strapi });
await modelsUtils.deleteContentTypes(modelsUIDs, { strapi });
await strapi.destroy();
});
+10 -6
View File
@@ -21,14 +21,18 @@ async function main(connection) {
await orm.schema.sync();
await orm.schema.reset();
await orm.query('article').createMany({
await orm.query('article').create({
// select: {},
// populate: {},
data: Array(5)
.fill({})
.map((v, idx) => ({
title: `Article ${_.padStart(idx, 3, '0')}`,
})),
data: {
compo: {
id: 1,
__pivot: {
order: 1,
field: 'compo',
},
},
},
});
const articles = await orm.query('article').findMany({
+15 -15
View File
@@ -38,23 +38,23 @@ const article = {
title: {
type: 'string',
},
category: {
type: 'relation',
relation: 'manyToOne',
target: 'category',
inversedBy: 'articles',
// useJoinTable: false,
},
tags: {
type: 'relation',
relation: 'manyToMany',
target: 'tag',
inversedBy: 'articles',
},
compos: {
// category: {
// type: 'relation',
// relation: 'manyToOne',
// target: 'category',
// inversedBy: 'articles',
// // useJoinTable: false,
// },
// tags: {
// type: 'relation',
// relation: 'manyToMany',
// target: 'tag',
// inversedBy: 'articles',
// },
compo: {
type: 'component',
component: 'compo',
repeatable: true,
// repeatable: true,
},
// cover: {
// type: 'media',
+17 -11
View File
@@ -105,7 +105,7 @@ const createEntityManager = db => {
.execute();
// create relation associations or move this to the entity service & call attach on the repo instead
await this.attachRelations(metadata, id, data);
await this.attachRelations(uid, id, data);
// TODO: in case there is not select or populate specified return the inserted data ?
@@ -169,7 +169,7 @@ const createEntityManager = db => {
.execute();
}
await this.updateRelations(metadata, id, data);
await this.updateRelations(uid, id, data);
return this.findOne(uid, { where: { id }, select: params.select, populate: params.populate });
},
@@ -218,7 +218,7 @@ const createEntityManager = db => {
.delete()
.execute();
await this.deleteRelations(metadata, id);
await this.deleteRelations(uid, id);
return entity;
},
@@ -265,8 +265,14 @@ const createEntityManager = db => {
* @param {object} data - data received for creation
*/
// TODO: wrap Transaction
async attachRelations(metadata, id, data) {
const { attributes } = metadata;
async attachRelations(uid, id, data) {
const { attributes } = db.metadata.get(uid);
/*
TODO:
if data[attributeName] is a single value (ID) => assign
if data[attributeName] is an object with an id => assign & use the other props as join column values
*/
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
@@ -277,7 +283,7 @@ const createEntityManager = db => {
isBidirectional(attribute) &&
data[attributeName]
) {
await this.createQueryBuilder(metadata.uid)
await this.createQueryBuilder(uid)
.where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
.update({ [attribute.joinColumn.name]: null })
.execute();
@@ -357,8 +363,8 @@ const createEntityManager = db => {
*/
// TODO: check relation exists (handled by FKs except for polymorphics)
// TODO: wrap Transaction
async updateRelations(metadata, id, data) {
const { attributes } = metadata;
async updateRelations(uid, id, data) {
const { attributes } = db.metadata.get(uid);
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
@@ -366,7 +372,7 @@ const createEntityManager = db => {
if (attribute.joinColumn && attribute.owner) {
// TODO: redefine
if (attribute.relation === 'oneToOne' && _.has(attributeName, data)) {
await this.createQueryBuilder(metadata.uid)
await this.createQueryBuilder(uid)
.where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
.update({ [attribute.joinColumn.name]: null })
.execute();
@@ -450,13 +456,13 @@ const createEntityManager = db => {
* @param {ID} id - entity ID
*/
// TODO: wrap Transaction
async deleteRelations(metadata, id) {
async deleteRelations(uid, id) {
// TODO: Implement correctly
if (db.dialect.usesForeignKeys()) {
return;
}
const { attributes } = metadata;
const { attributes } = db.metadata.get(uid);
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
@@ -83,6 +83,17 @@ const createRepository = (uid, db) => {
return db.entityManager.count(uid, params);
},
attachRelations(id, data) {
console.log(id, data)
return db.entityManager.attachRelations(uid, id, data);
},
updateRelations(id, data) {
return db.entityManager.updateRelations(uid, id, data);
},
deleteRelations(id) {
return db.entityManager.deleteRelations(uid, id);
},
// TODO: add relation API
populate() {},
+26 -4
View File
@@ -43,11 +43,33 @@ interface CreateManyParams<T> {
data: T[keyof T][];
}
interface Pagination {
page: number;
pageSize: number;
pageCount: number;
total: number;
}
interface QueryFromContentType<T extends keyof AllTypes> {
findOne(params: FindParams<AllTypes[T]>): AllTypes[T];
findMany(params: FindParams<AllTypes[T]>): AllTypes[T][];
create(params: CreateParams<AllTypes[T]>): AllTypes[T][];
createMany(params: CreateManyParams<AllTypes[T]>): AllTypes[T][];
findOne(params: FindParams<AllTypes[T]>): any;
findMany(params: FindParams<AllTypes[T]>): any[];
findWithCount(params: FindParams<AllTypes[T]>): [any[], number];
findPage(params: FindParams<AllTypes[T]>): { results: any[]; pagination: Pagination };
create(params: CreateParams<AllTypes[T]>): any;
createMany(params: CreateManyParams<AllTypes[T]>): { count: number };
update(params: any): any;
updateMany(params: any): { count: number };
delete(params: any): any;
deleteMany(params: any): { count: number };
count(params: any): number;
attachRelations(id: ID, data: any): any;
updateRelations(id: ID, data: any): any;
deleteRelations(id: ID): any;
}
interface ModelConfig {
+3 -7
View File
@@ -1,8 +1,6 @@
import { Database } from '@strapi/database';
import { Strapi } from './Strapi';
type StrapiInstance = InstanceType<typeof Strapi>;
type ID = number | string;
interface Options<T> {
@@ -36,21 +34,19 @@ interface EntityService {
countSearch<T extends keyof AllTypes>(uid: T): Promise<any>;
}
interface StrapiInterface {
interface StrapiInterface extends Strapi {
query: Database['query'];
entityService: EntityService;
}
type StrapiGlobal = StrapiInterface | StrapiInstance;
declare global {
interface AllTypes {}
}
declare global {
export interface Global {
strapi: StrapiGlobal;
strapi: StrapiInterface;
}
const strapi: StrapiGlobal;
const strapi: StrapiInterface;
}
@@ -1,6 +1,6 @@
'use strict';
const { pick } = require('lodash/fp');
const { has, pick } = require('lodash/fp');
const delegate = require('delegates');
const {
@@ -147,22 +147,29 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
// select / populate
const query = transformParamsToQuery(pickSelectionParams(params));
const entry = await db.query(uid).create({ ...query, data: validData });
// TODO: wrap into transaction
// TODO: implement files
const componentData = await createComponents(uid, validData);
const entity = await db.query(uid).create({
...query,
data: Object.assign(validData, componentData),
});
// TODO: Implement components CRUD
// TODO: implement files outside of the entity service
// if (files && Object.keys(files).length > 0) {
// await this.uploadFiles(entry, files, { model });
// entry = await this.findOne({ params: { id: entry.id } }, { model });
// }
// TODO: Implement components CRUD ?
eventHub.emit(ENTRY_CREATE, {
model: modelDef.modelName,
entry: sanitizeEntity(entry, { model: modelDef }),
entry: sanitizeEntity(entity, { model: modelDef }),
});
return entry;
return entity;
},
async update(uid, entityId, opts) {
@@ -182,7 +189,15 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
// select / populate
const query = transformParamsToQuery(pickSelectionParams(params));
let entry = await db.query(uid).update({ ...query, where: { id: entityId }, data });
// TODO: wrap in transaction
const componentData = await updateComponents(uid, data);
let entry = await db.query(uid).update({
...query,
where: { id: entityId },
data: Object.assign(data, componentData),
});
// TODO: implement files
// if (files && Object.keys(files).length > 0) {
@@ -261,3 +276,150 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
// return db.query(uid).countSearch(params);
},
});
// TODO: Generalize the logic to CRUD relation directly in the DB layer
const createComponents = async (uid, data) => {
const { attributes } = strapi.getModel(uid);
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
if (!has(attributeName, data)) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID, repeatable = false } = attribute;
const componentValue = data[attributeName];
if (componentValue === null) {
continue;
}
if (repeatable === true) {
if (!Array.isArray(componentValue)) {
throw new Error('Expected an array to create repeatable component');
}
const components = await Promise.all(
componentValue.map(value => {
return strapi.query(componentUID).create({ data: value });
})
);
return {
[attributeName]: components.map(({ id }, idx) => {
// TODO: add & support pivot data in DB
return id;
}),
};
} else {
const component = await strapi.query(componentUID).create({ data: componentValue });
return {
// TODO: add & support pivot data in DB
[attributeName]: component.id,
};
}
}
if (attribute.type === 'dynamiczone') {
const dynamiczoneValues = data[attributeName];
if (!Array.isArray(dynamiczoneValues)) {
throw new Error('Expected an array to create repeatable component');
}
const components = await Promise.all(
dynamiczoneValues.map(value => {
return strapi.query(value.__component).create({ data: value });
})
);
return {
[attributeName]: components.map(({ id }, idx) => {
// TODO: add & support pivot data in DB
return id;
}),
};
}
}
};
const updateOrCreateComponent = (componentUID, value) => {
// update
if (has('id', value)) {
return strapi.query(componentUID).update({ where: { id: value.id }, data: value });
}
// create
return strapi.query(componentUID).create({ data: value });
};
const updateComponents = async (uid, data) => {
// TODO: clear old -> done in the updateRelation
const { attributes } = strapi.getModel(uid);
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
if (!has(attributeName, data)) {
continue;
}
if (attribute.type === 'component') {
const { component: componentUID, repeatable = false } = attribute;
const componentValue = data[attributeName];
if (componentValue === null) {
continue;
}
if (repeatable === true) {
if (!Array.isArray(componentValue)) {
throw new Error('Expected an array to create repeatable component');
}
const components = await Promise.all(
componentValue.map(value => updateOrCreateComponent(componentUID, value))
);
return {
[attributeName]: components.map(({ id }, idx) => {
// TODO: add & support pivot data in DB
return id;
}),
};
} else {
const component = await updateOrCreateComponent(componentUID, componentValue);
return {
// TODO: add & support pivot data in DB
[attributeName]: component.id,
};
}
}
if (attribute.type === 'dynamiczone') {
const dynamiczoneValues = data[attributeName];
if (!Array.isArray(dynamiczoneValues)) {
throw new Error('Expected an array to create repeatable component');
}
const components = await Promise.all(
dynamiczoneValues.map(value => updateOrCreateComponent(value.__component, value))
);
return {
[attributeName]: components.map(({ id }, idx) => {
// TODO: add & support pivot data in DB
return id;
}),
};
}
}
};
@@ -79,6 +79,9 @@ describe('Core API - Basic + compo', () => {
method: 'POST',
url: '/product-with-compos',
body: product,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@@ -91,6 +94,9 @@ describe('Core API - Basic + compo', () => {
const res = await rq({
method: 'GET',
url: '/product-with-compos',
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@@ -115,6 +121,9 @@ describe('Core API - Basic + compo', () => {
method: 'PUT',
url: `/product-with-compos/${data.productsWithCompo[0].id}`,
body: product,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@@ -128,6 +137,9 @@ describe('Core API - Basic + compo', () => {
const res = await rq({
method: 'DELETE',
url: `/product-with-compos/${data.productsWithCompo[0].id}`,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@@ -244,7 +244,7 @@ describe('Create Strapi API End to End', () => {
});
afterAll(async () => {
await modelsUtils.cleanupModels(['article', 'category'], { strapi });
await modelsUtils.cleanupModels([form.article.uid, form.category.uid], { strapi });
});
test('Create cat1', async () => {
@@ -497,7 +497,7 @@ describe('Create Strapi API End to End', () => {
});
afterAll(async () => {
await modelsUtils.cleanupModels(['article', 'reference'], { strapi });
await modelsUtils.cleanupModels([form.article.uid, form.reference.uid], { strapi });
});
test('Create ref1', async () => {
@@ -592,7 +592,7 @@ describe('Create Strapi API End to End', () => {
});
afterAll(async () => {
await modelsUtils.cleanupModels(['reference', 'tag'], { strapi });
await modelsUtils.cleanupModels([form.reference.uid, form.tag.uid], { strapi });
});
test('Attach Tag to a Reference', async () => {
+3 -3
View File
@@ -25,7 +25,7 @@ module.exports = {
createdModel = await modelsUtils.createContentType(contentType);
ctx.addModel(createdModel);
},
cleanup: () => modelsUtils.deleteContentType(createdModel.modelName),
cleanup: () => modelsUtils.deleteContentType(createdModel.uid),
};
},
@@ -39,7 +39,7 @@ module.exports = {
},
async cleanup() {
for (const model of createdModels) {
await modelsUtils.deleteContentType(model.modelName);
await modelsUtils.deleteContentType(model.uid);
}
},
};
@@ -59,7 +59,7 @@ module.exports = {
},
async cleanup() {
for (const model of createdModels) {
await modelsUtils.deleteContentType(model.modelName);
await modelsUtils.deleteContentType(model.uid);
}
},
};
+1 -1
View File
@@ -77,7 +77,7 @@ const createTestBuilder = (options = {}) => {
if (enableTestDataAutoCleanup) {
for (const model of models.reverse()) {
await modelsUtils.cleanupModel(model.modelName);
await modelsUtils.cleanupModel(model.uid);
}
}
+6
View File
@@ -21,6 +21,7 @@ module.exports = {
targetAttribute: 'articles',
},
},
uid: 'application::article.article',
name: 'article',
description: '',
collectionName: '',
@@ -37,6 +38,7 @@ module.exports = {
targetAttribute: 'tags',
},
},
uid: 'application::tag.tag',
name: 'tag',
description: '',
collectionName: '',
@@ -52,6 +54,7 @@ module.exports = {
targetAttribute: 'category',
},
},
uid: 'application::category.category',
name: 'category',
description: '',
collectionName: '',
@@ -71,6 +74,7 @@ module.exports = {
target: 'application::tag.tag',
},
},
uid: 'application::reference.reference',
name: 'reference',
description: '',
collectionName: 'refs',
@@ -87,6 +91,7 @@ module.exports = {
type: 'boolean',
},
},
uid: 'application::product.product',
name: 'product',
description: '',
collectionName: '',
@@ -101,6 +106,7 @@ module.exports = {
target: 'application::tag.tag',
},
},
uid: 'application::articlewit.articlewit',
name: 'articlewithtag',
description: '',
collectionName: '',
+5 -7
View File
@@ -98,9 +98,8 @@ const deleteComponents = async (componentsUID, { strapi } = {}) => {
return deletedComponents;
};
const deleteContentType = async (modelName, { strapi } = {}) => {
const deleteContentType = async (uid, { strapi } = {}) => {
const { contentTypeService, cleanup } = await createHelpers({ strapi });
const uid = `application::${modelName}.${modelName}`;
const contentType = await contentTypeService.deleteContentType(uid);
@@ -109,11 +108,10 @@ const deleteContentType = async (modelName, { strapi } = {}) => {
return contentType;
};
const deleteContentTypes = async (modelsName, { strapi } = {}) => {
const deleteContentTypes = async (modelsUIDs, { strapi } = {}) => {
const { contentTypeService, cleanup } = await createHelpers({ strapi });
const toUID = name => `application::${name}.${name}`;
const contentTypes = await contentTypeService.deleteContentTypes(modelsName.map(toUID));
const contentTypes = await contentTypeService.deleteContentTypes(modelsUIDs);
await cleanup();
@@ -126,10 +124,10 @@ async function cleanupModels(models, { strapi } = {}) {
}
}
async function cleanupModel(model, { strapi: strapiIst } = {}) {
async function cleanupModel(uid, { strapi: strapiIst } = {}) {
const { strapi, cleanup } = await createHelpers({ strapi: strapiIst });
await strapi.query(`application::${model}.${model}`).deleteMany();
await strapi.query(uid).deleteMany();
await cleanup();
}