Crud + relations Associations v1

This commit is contained in:
Alexandre Bodin 2021-06-25 12:07:32 +02:00
parent bdbf90b567
commit d33a363c88
8 changed files with 303 additions and 212 deletions

View File

@ -41,17 +41,17 @@ module.exports = async () => {
registerAdminConditions();
registerPermissionActions();
const permissionService = getService('permission');
// const permissionService = getService('permission');
const userService = getService('user');
const roleService = getService('role');
await roleService.createRolesIfNoneExist();
await roleService.resetSuperAdminPermissions();
// await roleService.resetSuperAdminPermissions();
await roleService.displayWarningIfNoSuperAdmin();
await permissionService.ensureBoundPermissionsInDatabase();
await permissionService.cleanPermissionsInDatabase();
// await permissionService.ensureBoundPermissionsInDatabase();
// await permissionService.cleanPermissionsInDatabase();
await userService.displayWarningIfUsersDontHaveRole();

View File

@ -256,27 +256,14 @@ const deleteByIds = async ids => {
/** Count the users that don't have any associated roles
* @returns {Promise<number>}
*/
// FIXME: test / cleanup
const countUsersWithoutRole = async () => {
return strapi.query('strapi::user').count({ where: { roles: { id: { $null: true } } } });
// const userModel = strapi.query('strapi::user').model;
// let count;
// if (userModel.orm === 'bookshelf') {
// count = await strapi.query('strapi::user').count({ roles_null: true });
// } else if (userModel.orm === 'mongoose') {
// count = await strapi.query('strapi::user').model.countDocuments({
// $or: [{ roles: { $exists: false } }, { roles: { $size: 0 } }],
// });
// } else {
// const allRoles = await strapi.query('role', 'admin').find({ _limit: -1 });
// count = await strapi.query('strapi::user').count({
// roles_nin: allRoles.map(r => r.id),
// });
// }
// return count;
return strapi.query('strapi::user').count({
where: {
roles: {
id: { $null: true },
},
},
});
};
/**
@ -291,13 +278,7 @@ const count = async (where = {}) => {
/** Assign some roles to several users
* @returns {undefined}
*/
// TODO: impl with updateMany
const assignARoleToAll = async roleId => {
// await strapi.query('strapi::user').updateMany({
// where: { roles: { id: { $null: true } } },
// data: { roles: [roleId] },
// });
const users = await strapi.query('strapi::user').findMany({
select: ['id'],
where: {
@ -305,42 +286,12 @@ const assignARoleToAll = async roleId => {
},
});
console.log(users);
await strapi.query('strapi;:role').update({
await strapi.query('strapi::role').update({
where: { id: roleId },
data: {
users: users.map(u => u.id),
},
});
// for (const user of users) {
// await strapi
// .query('strapi::user')
// .update({ where: { id: user.id }, data: { roles: [roleId] } });
// // or
// }
// const userModel = strapi.query('strapi::user').model;
// if (userModel.orm === 'bookshelf') {
// const assocTable = userModel.associations.find(a => a.alias === 'roles').tableCollectionName;
// const userTable = userModel.collectionName;
// const knex = strapi.connections[userModel.connection];
// const usersIds = await knex
// .select(`${userTable}.id`)
// .from(userTable)
// .leftJoin(assocTable, `${userTable}.id`, `${assocTable}.user_id`)
// .where(`${assocTable}.role_id`, null)
// .pluck(`${userTable}.id`);
// if (usersIds.length > 0) {
// const newRelations = usersIds.map(userId => ({ user_id: userId, role_id: roleId }));
// await knex.insert(newRelations).into(assocTable);
// }
// } else if (userModel.orm === 'mongoose') {
// await strapi.query('strapi::user').model.updateMany({}, { roles: [roleId] });
// }
};
/** Display a warning if some users don't have at least one role

View File

@ -19,32 +19,6 @@ async function main() {
await orm.schema.reset();
// await orm.schema.sync();
// const article = await orm.query('article').create({
// data: {
// title: 'Test',
// },
// });
// // console.log(article);
// await orm.query('article').findOne({
// where: {
// id: 2,
// },
// });
// await orm.query('article').update({
// data: {
// title: 'Test',
// },
// });
// await orm.query('article').delete({
// where: {
// id: 2,
// },
// });
} finally {
orm.destroy();
}

View File

@ -22,11 +22,6 @@ const category = {
relation: 'oneToMany',
target: 'article',
mappedBy: 'category',
// useJoinTable: false,
},
compo: {
type: 'component',
component: 'compo',
},
},
};
@ -52,7 +47,7 @@ const article = {
target: 'tag',
inversedBy: 'articles',
},
compo: {
compos: {
type: 'component',
component: 'compo',
repeatable: true,
@ -94,7 +89,7 @@ const compo = {
type: 'string',
},
value: {
type: 'integer',
type: 'string',
},
},
};

View File

@ -26,7 +26,10 @@ const pickRowAttributes = (metadata, data = {}) => {
// TODO: ensure joinColumn name respect convention ?
const joinColumnName = attribute.joinColumn.name;
const attrValue = data[attributeName] || data[joinColumnName];
// allow setting to null
const attrValue = !_.isUndefined(data[attributeName])
? data[attributeName]
: data[joinColumnName];
if (!_.isUndefined(attrValue)) {
obj[joinColumnName] = attrValue;
@ -38,6 +41,45 @@ const pickRowAttributes = (metadata, data = {}) => {
return obj;
};
/**
* Attach relations to a new entity
* oneToOne
* if owner
* if joinColumn
* -> Id should have been added in the column of the model table beforehand to avoid extra updates
* if joinTable
* -> add relation
*
* if not owner
* if joinColumn
* -> add relation
* if joinTable
* -> add relation in join table
*
* oneToMany
* owner -> cannot be owner
* not owner
* joinColumn
* -> add relations in target
* joinTable
* -> add relations in join table
*
* manyToOne
* not owner -> must be owner
* owner
* join Column
* -> Id should have been added in the column of the model table beforehand to avoid extra updates
* joinTable
* -> add relation in join table
*
* manyToMany
* -> add relation in join table
*
* @param {EntityManager} em - entity manager instance
* @param {Metadata} metadata - model metadta
* @param {ID} id - entity ID
* @param {object} data - data received for creation
*/
const attachRelations = async (em, metadata, id, data) => {
const { attributes } = metadata;
@ -80,11 +122,13 @@ const attachRelations = async (em, metadata, id, data) => {
return {
[joinColumn.name]: id,
[inverseJoinColumn.name]: datum,
...(joinTable.on || {}),
};
})
: {
[joinColumn.name]: id,
[inverseJoinColumn.name]: data[attributeName],
...(joinTable.on || {}),
};
await em
@ -93,49 +137,167 @@ const attachRelations = async (em, metadata, id, data) => {
.execute();
}
}
/*
oneToOne
if owner
if joinColumn
TODO: We might actually want to make the column unique and throw -> doing this makes the code generic and doesn't require specific logic
removing existing relation
-> Id should have been added in the column of the model table beforehand to avoid extra updates
if joinTable
-> clear join Table assoc
-> add relation
if not owner
if joinColumn
remove existing relation
-> add relation
if joinTable
-> clear join Table assoc
-> add relation in join table
oneToMany
owner -> cannot be owner
not owner
joinColumn
-> add relations in target
joinTable
-> add relations in join table
manyToOne
not owner -> must be owner
owner
join Column
-> Id should have been added in the column of the model table beforehand to avoid extra updates
joinTable
-> add relation in join table
manyToMany
-> add relation in join table
*/
}
};
/**
* Updates relations of an existing entity
* oneToOne
* if owner
* if joinColumn
* -> handled in the DB row
* if joinTable
* -> clear join Table assoc
* -> add relation in join table
*
* if not owner
* if joinColumn
* -> set join column on the target
* if joinTable
* -> clear join Table assoc
* -> add relation in join table
*
* oneToMany
* owner -> cannot be owner
* not owner
* joinColumn
* -> set join column on the target
* joinTable
* -> add relations in join table
*
* manyToOne
* not owner -> must be owner
* owner
* join Column
* -> handled in the DB row
* joinTable
* -> add relation in join table
*
* manyToMany
* -> clear join Table assoc
* -> add relation in join table
*
* @param {EntityManager} em - entity manager instance
* @param {Metadata} metadata - model metadta
* @param {ID} id - entity ID
* @param {object} data - data received for creation
*/
// TODO: check relation exists (handled by FKs except for polymorphics)
const updateRelations = async (em, metadata, id, data) => {
const { attributes } = metadata;
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
// NOTE: we do not remove existing associations with the target as it should handled by unique FKs instead
if (attribute.joinColumn && attribute.owner) {
// nothing to do => relation already added on the table
continue;
}
// oneToOne oneToMany on the non owning side.
// Since it is a join column no need to remove previous relations
if (attribute.joinColumn && !attribute.owner) {
// need to set the column on the target
const { target } = attribute;
if (data[attributeName]) {
await em
.createQueryBuilder(target)
.update({ [attribute.joinColumn.referencedColumn]: id })
// NOTE: works if it is an array or a single id
.where({ id: data[attributeName] })
.execute();
}
}
if (attribute.joinTable) {
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
if (data[attributeName]) {
// clear previous associations in the joinTable
await em
.createQueryBuilder(joinTable.name)
.delete()
.where({
[joinColumn.name]: id,
})
// TODO: add join.on filters to only clear the valid info
.where(joinTable.on ? joinTable.on : {})
.execute();
// TODO: add pivot informations too
const insert = Array.isArray(data[attributeName])
? data[attributeName].map(datum => {
return {
[joinColumn.name]: id,
[inverseJoinColumn.name]: datum,
...(joinTable.on || {}),
};
})
: {
[joinColumn.name]: id,
[inverseJoinColumn.name]: data[attributeName],
...(joinTable.on || {}),
};
console.log(insert);
await em
.createQueryBuilder(joinTable.name)
.insert(insert)
.execute();
}
}
}
};
/**
* Delete relations of an existing entity
* This removes associations but doesn't do cascade deletions for components for example. This will be handled on the entity service layer instead
* NOTE: Most of the deletion should be handled by ON DELETE CASCADE
*
* oneToOne
* if owner
* if joinColumn
* -> handled in the DB row
* if joinTable
* -> clear join Table assoc
*
* if not owner
* if joinColumn
* -> set join column on the target // CASCADING should do the job
* if joinTable
* -> clear join Table assoc // CASCADING
*
* oneToMany
* owner -> cannot be owner
* not owner
* joinColumn
* -> set join column on the target
* joinTable
* -> add relations in join table
*
* manyToOne
* not owner -> must be owner
* owner
* join Column
* -> handled in the DB row
* joinTable
* -> add relation in join table
*
* manyToMany
* -> clear join Table assoc
* -> add relation in join table
*
* @param {EntityManager} em - entity manager instance
* @param {Metadata} metadata - model metadta
* @param {ID} id - entity ID
*/
// noop as cascade FKs does the job
const deleteRelations = () => {};
const createEntityManager = db => {
const repoMap = {};
@ -161,7 +323,6 @@ const createEntityManager = db => {
return await Promise.all([this.findMany(uid, params), this.count(uid, params)]);
},
// TODO: define api
async count(uid, params = {}) {
const qb = this.createQueryBuilder(uid).where(params.where);
@ -173,23 +334,24 @@ const createEntityManager = db => {
return Number(res.count);
},
// TODO: make it create one somehow
async create(uid, params) {
async create(uid, params = {}) {
// create entry in DB
const metadata = db.metadata.get(uid);
const { data } = params;
if (!_.isPlainObject(data)) {
throw new Error('Create expects a data object');
}
// transform value to storage value
// apply programatic defaults if any -> I think this should be handled outside of this layer as we might have some applicative rules in the entity service
// TODO: in query builder ?
const dataToInsert = pickRowAttributes(metadata, data);
if (_.isEmpty(dataToInsert)) {
throw new Error('Create requires data');
}
// if (_.isEmpty(dataToInsert)) {
// throw new Error('Create requires data');
// }
const [id] = await this.createQueryBuilder(uid)
.insert(dataToInsert)
@ -198,19 +360,25 @@ const createEntityManager = db => {
// create relation associations or move this to the entity service & call attach on the repo instead
await attachRelations(this, metadata, id, data);
// TODO: in case there is not select or populate specified return the inserted data ?
return this.findOne(uid, { where: { id }, select: params.select, populate: params.populate });
},
async createMany(uid, params) {
// TODO: where do we handle relation processing for many queries ?
async createMany(uid, params = {}) {
const { data } = params;
if (!_.isArray(data)) {
throw new Error('CreateMany expecets data to be an array');
}
const metadata = db.metadata.get(uid);
// Add defaults / transform to storage type
const dataToInsert = data.map(datum => pickRowAttributes(metadata, datum));
if (_.isEmpty(dataToInsert)) {
throw new Error('Create requires data');
throw new Error('Nothing to insert');
}
await this.createQueryBuilder(uid)
@ -220,9 +388,43 @@ const createEntityManager = db => {
return { count: data.length };
},
// TODO: make it update one somehow
// findOne + update with a return
async update(uid, params) {
async update(uid, params = {}) {
const { where, data } = params;
const metadata = db.metadata.get(uid);
if (_.isEmpty(where)) {
throw new Error('Update requires a where parameter');
}
const entity = await this.createQueryBuilder(uid)
.select('id')
.where(where)
.first()
.execute();
if (!entity) {
// TODO: or throw ?
return null;
}
const { id } = entity;
const dataToUpdate = pickRowAttributes(metadata, data);
if (!_.isEmpty(dataToUpdate)) {
await this.createQueryBuilder(uid)
.where({ id })
.update(dataToUpdate)
.execute();
}
await updateRelations(this, metadata, id, data);
return this.findOne(uid, { where: { id }, select: params.select, populate: params.populate });
},
// TODO: where do we handle relation processing for many queries ?
async updateMany(uid, params = {}) {
const { where, data } = params;
const metadata = db.metadata.get(uid);
@ -232,66 +434,54 @@ const createEntityManager = db => {
throw new Error('Update requires data');
}
const res = await this.createQueryBuilder(uid)
const updatedRows = await this.createQueryBuilder(uid)
.where(where)
.update(dataToUpdate)
.execute();
// TODO: update relations
console.log({ res });
// TODO: return obj
return {};
return { count: updatedRows };
},
// only returns the number of affected rows
async updateMany(uid, params) {
const { where, data } = params;
async delete(uid, params = {}) {
const { where, select, populate } = params;
const metadata = db.metadata.get(uid);
const dataToUpdate = pickRowAttributes(metadata, data);
if (_.isEmpty(dataToUpdate)) {
throw new Error('Update requires data');
if (_.isEmpty(where)) {
throw new Error('Delete requires a where parameter');
}
const res = await this.createQueryBuilder(uid)
.where(where)
.update(dataToUpdate)
.execute();
const entity = await this.findOne(uid, {
where,
select: select && ['id'].concat(select),
populate,
});
console.log({ res });
if (!entity) {
return null;
}
// TODO: update relations
const { id } = entity;
// TODO: Return count on updateMany
},
// TODO: make it deleteOne somehow
// findOne + delete with a return -> should go in the entity service
async delete(uid, params) {
const res = await this.createQueryBuilder(uid)
.init(params)
await this.createQueryBuilder(uid)
.where({ id })
.delete()
.execute();
console.log({ res });
// TODO: delete relations
await deleteRelations(this, metadata, id);
return res;
return entity;
},
async deleteMany(uid, params) {
// TODO: where do we handle relation processing for many queries ?
async deleteMany(uid, params = {}) {
const { where } = params;
const res = await this.createQueryBuilder(uid)
const deletedRows = await this.createQueryBuilder(uid)
.where(where)
.delete()
.execute();
// TODO: delete relations
return res;
return { count: deletedRows };
},
// populate already loaded entry

View File

@ -389,7 +389,7 @@ const createCompoLinkModelMeta = baseModelMeta => {
return {
// TODO: make sure there can't be any conflicts with a prefix
// singularName: 'compo',
uid: `${baseModelMeta.uid}_components`,
uid: `${baseModelMeta.tableName}_components`,
tableName: `${baseModelMeta.tableName}_components`,
attributes: {
id: {

View File

@ -280,7 +280,6 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
return qb.where(column, '<>', value);
}
case '$gt': {
return qb.where(column, '>', value);
}
@ -293,20 +292,16 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
case '$lte': {
return qb.where(column, '<=', value);
}
case '$null': {
return value === true ? qb.whereNull() : qb.whereNotNull();
return value === true ? qb.whereNull(column) : qb.whereNotNull(column);
}
case '$between': {
return qb.whereBetween(column, value);
}
case '$regexp': {
// TODO:
return;
}
// string
// TODO: use $case to make it case insensitive
case '$like': {
@ -447,22 +442,20 @@ const processPopulate = (populate, ctx) => {
const applyPopulate = async (results, populate, ctx) => {
// TODO: cleanup code
// TODO: create aliases for pivot columns
// TODO: remove joinColumn
// TODO: optimize depth to avoid overfetching
// TODO: ⚠️ on join tables we might want to make one query to find all the xxx_id then one query instead of a join to avoid returning multiple times the same object
const { db, uid, qb } = ctx;
const meta = db.metadata.get(uid);
// TODO: support deep populates
for (const key in populate) {
const populateValue = populate[key];
const attribute = meta.attributes[key];
const targetMeta = db.metadata.get(attribute.target);
// TODO: use query builder directly ?
// will need some specific code per relation
if (attribute.relation === 'oneToOne' || attribute.relation === 'manyToOne') {
if (attribute.joinColumn) {
const {
@ -497,7 +490,6 @@ const applyPopulate = async (results, populate, ctx) => {
const alias = qb.getAlias();
const rr = await qb
.init(populateValue)
.select('*')
.join({
alias: alias,
referencedTable: joinTable.name,
@ -550,16 +542,12 @@ const applyPopulate = async (results, populate, ctx) => {
if (attribute.joinTable) {
const { joinTable } = attribute;
// query the target through the join table
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
// TODO: create aliases for the columns
const alias = qb.getAlias();
const rr = await qb
.init(populateValue)
.select('*')
.join({
alias: alias,
referencedTable: joinTable.name,
@ -568,7 +556,6 @@ const applyPopulate = async (results, populate, ctx) => {
rootTable: qb.alias,
on: joinTable.on,
})
//TODO: select join column
.addSelect(`${alias}.${joinTable.joinColumn.name}`)
.where({
[`${alias}.${joinTable.joinColumn.name}`]: results.map(
@ -590,16 +577,12 @@ const applyPopulate = async (results, populate, ctx) => {
continue;
} else if (attribute.relation === 'manyToMany') {
const { joinTable } = attribute;
// query the target through the join table
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
// TODO: create aliases for the columns
const alias = qb.getAlias();
const rr = await qb
.init(populateValue)
.select('*')
.join({
alias: alias,
referencedTable: joinTable.name,
@ -618,12 +601,13 @@ const applyPopulate = async (results, populate, ctx) => {
const rrMap = _.groupBy(joinTable.joinColumn.name, rr);
// TODO: remove joinColumn
results.forEach(r => {
Object.assign(r, {
[key]: rrMap[r[joinTable.joinColumn.referencedColumn]] || [],
});
});
continue;
}
}
};

View File

@ -244,17 +244,14 @@ const createQueryBuilder = (uid, db) => {
helpers.applyJoins(qb, state.joins);
}
// TODO: hanlde populate
console.log('Running query: ', qb.toSQL());
// console.log('Running query: ', qb.toQuery());
const queryResult = await qb;
const results = db.dialect.processResult(queryResult, state.type);
// if query response should be process (in case of custom queries we shouldn't for example)
if (state.populate) {
// TODO: hanlde populate
await helpers.applyPopulate(_.castArray(results), state.populate, { qb: this, uid, db });
}