This commit is contained in:
Alexandre Bodin 2021-07-01 14:32:50 +02:00
parent 1728e67e5d
commit 03988acb63
6 changed files with 352 additions and 348 deletions

View File

@ -1,21 +1,7 @@
'use strict';
// use some middleware stack or "use" to add extensions to the db layer somehow
const wrapDebug = obj => {
const nOjb = {};
for (const key in obj) {
nOjb[key] = async function(...args) {
const result = await obj[key](...args);
// console.log(`[${key}]:`, result);
return result;
};
}
return nOjb;
};
const createRepository = (uid, db) =>
wrapDebug({
const createRepository = (uid, db) => {
return {
findOne(params) {
return db.entityManager.findOne(uid, params);
},
@ -56,26 +42,12 @@ const createRepository = (uid, db) =>
return db.entityManager.count(uid, params);
},
// TODO: add relation API
populate() {},
load() {},
// TODO: TBD
aggregates: {
sum() {},
min() {},
max() {},
avg() {},
count() {},
groupBy() {},
},
// TODO: TBD
relations: {
attach() {},
detach() {},
set() {},
},
});
};
};
module.exports = {
createRepository,

View File

@ -9,8 +9,6 @@ const { createEntityManager } = require('./entity-manager');
// TODO: move back into strapi
const { transformContentTypes } = require('./utils/content-types');
// const Configuration = require('./configuration');
// const { resolveConnector } = require('./connector');
class Database {
constructor(config) {
@ -19,7 +17,6 @@ class Database {
// TODO: validate meta
// this.metadata.validate();
// this.connector = resolveConnector(this.config);
this.config = config;
this.dialect = getDialect(this);
@ -47,6 +44,7 @@ class Database {
}
}
// TODO: move into strapi
Database.transformContentTypes = transformContentTypes;
Database.init = async config => {
const db = new Database(config);

View File

@ -1,119 +1,14 @@
/**
* @module metadata
*
*/
'use strict';
const _ = require('lodash/fp');
const types = require('../types');
const hasComponentsOrDz = model => {
return Object.values(model.attributes).some(
({ type }) => types.isComponent(type) || types.isDynamicZone(type)
);
};
const hasInversedBy = _.has('inversedBy');
const hasMappedBy = _.has('mappedBy');
const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(attribute);
const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute);
const shouldUseJoinTable = attribute => attribute.useJoinTable !== false;
// TODO: how do we make sure this column is created ? should it be added in the attributes ? should the schema layer do the conversion ?
const createJoinColum = (metadata, { attribute /*attributeName, meta */ }) => {
const targetMeta = metadata.get(attribute.target);
const joinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
const joinColumn = {
name: joinColumnName,
referencedColumn: 'id',
referencedTable: targetMeta.tableName,
};
Object.assign(attribute, { owner: true, joinColumn });
if (isBidirectional(attribute)) {
const inverseAttribute = targetMeta.attributes[attribute.inversedBy];
// NOTE: do not invert here but invert in the query ?
Object.assign(inverseAttribute, {
joinColumn: {
name: joinColumn.referencedColumn,
referencedColumn: joinColumn.name,
},
});
}
};
const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
const targetMeta = metadata.get(attribute.target);
if (!targetMeta) {
throw new Error(`Unknow target ${attribute.target}`);
}
const joinTableName = _.snakeCase(`${meta.tableName}_${attributeName}_links`);
const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
const inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
metadata.add({
uid: joinTableName,
tableName: joinTableName,
attributes: {
[joinColumnName]: {
type: 'integer',
column: {
unsigned: true,
},
},
[inverseJoinColumnName]: {
type: 'integer',
column: {
unsigned: true,
},
},
// TODO: add extra pivot attributes -> user should use an intermediate entity
},
foreignKeys: [
{
name: `${joinTableName}_fk`,
columns: [joinColumnName],
referencedColumns: ['id'],
referencedTable: meta.tableName,
onDelete: 'CASCADE',
},
{
name: `${joinTableName}_inv_fk`,
columns: [inverseJoinColumnName],
referencedColumns: ['id'],
referencedTable: targetMeta.tableName,
onDelete: 'CASCADE',
},
],
});
const joinTable = {
name: joinTableName,
joinColumn: {
name: joinColumnName,
referencedColumn: 'id',
},
inverseJoinColumn: {
name: inverseJoinColumnName,
referencedColumn: 'id',
},
};
Object.assign(attribute, { joinTable });
if (isBidirectional(attribute)) {
const inverseAttribute = targetMeta.attributes[attribute.inversedBy];
Object.assign(inverseAttribute, {
joinTable: {
name: joinTableName,
joinColumn: joinTable.inverseJoinColumn,
inverseJoinColumn: joinTable.joinColumn,
},
});
}
};
const { createRelation } = require('./relations');
class Metadata extends Map {
add(meta) {
@ -121,38 +16,40 @@ class Metadata extends Map {
}
}
/**
* Create Metadata from models configurations
*
* timestamps => not optional anymore but auto added. Auto added on the content type or in the db layer ?
*
* options => options are handled on the layer above. Options convert to fields on the CT
*
* filters => not in v1
*
* attributes
*
* - type
* - mapping field name - column name
* - mapping field type - column type
* - formatter / parser => coming from field type so no
* - indexes / checks / contstraints
* - relations => reference to the target model (function or string to avoid circular deps ?)
* - name of the LEFT/RIGHT side foreign keys
* - name of join table
*
* - compo/dz => reference to the components
* - validators
* - hooks
* - default value
* - required -> should add a not null option instead of the API required
* - unique -> should add a DB unique option instead of the unique in the API (Unique by locale or something else for example)
*
* lifecycles
*
* private fields ? => handled on a different layer
* @param {object[]} models
* @returns {Metadata}
*/
const createMetadata = (models = []) => {
/*
timestamps => not optional anymore but auto added. Auto added on the content type or in the db layer ?
options => options are handled on the layer above. Options convert to fields on the CT
filters => not in v1
attributes
- type
- mapping field name - column name
- mapping field type - column type
- formatter / parser => coming from field type so no
- indexes / checks / contstraints
- relations => reference to the target model (function or string to avoid circular deps ?)
- name of the LEFT/RIGHT side foreign keys
- name of join table
- compo/dz => reference to the components
- validators
- hooks
- default value
- required -> should add a not null option instead of the API required
- unique -> should add a DB unique option instead of the unique in the API (Unique by locale or something else for example)
lifecycles
private fields ? => handled on a different layer
*/
// TODO: reorder to make sure we can create everything or delete everything in the right order
// TODO: allow passing the join config in the attribute
// TODO: allow passing column config in the attribute
@ -233,158 +130,10 @@ const createMetadata = (models = []) => {
return metadata;
};
const createRelation = (attributeName, attribute, meta, metadata) => {
switch (attribute.relation) {
case 'oneToOne': {
/*
if one to one then
if owner then
if with join table then
create join table
else
create joinColumn
if bidirectional then
set inverse attribute joinCol or joinTable info correctly
else
this property must be set by the owner side
verify the owner side is valid // should be done before or at the same time ?
*/
if (isOwner(attribute)) {
if (shouldUseJoinTable(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
createJoinColum(metadata, {
attribute,
attributeName,
meta,
});
}
} else {
// verify other side is valid
}
break;
}
case 'oneToMany': {
/*
if one to many then
if unidirectional then
create join table
if bidirectional then
cannot be owning side
do nothing
*/
if (!isBidirectional(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
if (isOwner(attribute)) {
throw new Error(
'one side of a oneToMany cannot be the owner side in a bidirectional relation'
);
}
}
break;
}
case 'manyToOne': {
/*
if many to one then
if unidirectional then
if with join table then
create join table
else
create join column
else
must be the owner side
if with join table then
create join table
else
create join column
set inverse attribute joinCol or joinTable info correctly
*/
if (isBidirectional(attribute) && !isOwner(attribute)) {
throw new Error('The many side of a manyToOne must be the owning side');
}
if (shouldUseJoinTable(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
createJoinColum(metadata, {
attribute,
attributeName,
meta,
});
}
break;
}
case 'manyToMany': {
/*
if many to many then
if unidirectional
create join table
else
if owner then
if with join table then
create join table
else
do nothing
*/
if (!isBidirectional(attribute) || isOwner(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
}
break;
}
default: {
throw new Error(`Unknow relation ${attribute.relation}`);
}
}
/*
polymorphic relations
OneToOneX
ManyToOneX
OnetoManyX
ManytoManyX
XOneToOne
XManyToOne
XOnetoMany
XManytoMany
XOneToOneX
XManyToOneX
XOnetoManyX
XManytoManyX
*/
const hasComponentsOrDz = model => {
return Object.values(model.attributes).some(
({ type }) => types.isComponent(type) || types.isDynamicZone(type)
);
};
// NOTE: we might just move the compo logic outside this layer too at some point

View File

@ -0,0 +1,299 @@
/**
* @module relations
*/
'use strict';
const _ = require('lodash/fp');
/**
* Creates a relation metadata
*
* @param {string} attributeName
* @param {Attribute} attribute
* @param {ModelMetadata} meta
* @param {Metadata} metadata
*/
const createRelation = (attributeName, attribute, meta, metadata) => {
if (_.has(attribute.relation, relationFactoryMap)) {
return relationFactoryMap[attribute.relation](attributeName, attribute, meta, metadata);
}
throw new Error(`Unknow relation ${attribute.relation}`);
/*
polymorphic relations
OneToOneX
ManyToOneX
OnetoManyX
ManytoManyX
XOneToOne
XManyToOne
XOnetoMany
XManytoMany
XOneToOneX
XManyToOneX
XOnetoManyX
XManytoManyX
*/
};
/**
* Creates a oneToOne relation metadata
*
* if owner then
* if with join table then
* create join table
* else
* create joinColumn
* if bidirectional then
* set inverse attribute joinCol or joinTable info correctly
* else
* this property must be set by the owner side
* verify the owner side is valid // should be done before or at the same time ?
*
* @param {string} attributeName
* @param {Attribute} attribute
* @param {ModelMetadata} meta
* @param {Metadata} metadata
* @retuns void
*/
const createOneToOne = (attributeName, attribute, meta, metadata) => {
if (isOwner(attribute)) {
if (shouldUseJoinTable(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
createJoinColum(metadata, {
attribute,
attributeName,
meta,
});
}
} else {
// TODO: verify other side is valid
}
};
/**
* Creates a oneToMany relation metadata
*
* if unidirectional then
* create join table
* if bidirectional then
* cannot be owning side
* do nothing
*
* @param {string} attributeName
* @param {Attribute} attribute
* @param {ModelMetadata} meta
* @param {Metadata} metadata
*/
const createOneToMany = (attributeName, attribute, meta, metadata) => {
if (!isBidirectional(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
if (isOwner(attribute)) {
throw new Error(
'one side of a oneToMany cannot be the owner side in a bidirectional relation'
);
}
}
};
/**
* Creates a manyToOne relation metadata
*
* if unidirectional then
* if with join table then
* create join table
* else
* create join column
* else
* must be the owner side
* if with join table then
* create join table
* else
* create join column
* set inverse attribute joinCol or joinTable info correctly
*
* @param {string} attributeName
* @param {Attribute} attribute
* @param {ModelMetadata} meta
* @param {Metadata} metadata
*/
const createManyToOne = (attributeName, attribute, meta, metadata) => {
if (isBidirectional(attribute) && !isOwner(attribute)) {
throw new Error('The many side of a manyToOne must be the owning side');
}
if (shouldUseJoinTable(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
} else {
createJoinColum(metadata, {
attribute,
attributeName,
meta,
});
}
};
/**
* Creates a manyToMany relation metadata
*
* if unidirectional
* create join table
* else
* if owner then
* if with join table then
* create join table
* else
* do nothing
*
* @param {string} attributeName
* @param {Attribute} attribute
* @param {ModelMetadata} meta
* @param {Metadata} metadata
*/
const createManyToMany = (attributeName, attribute, meta, metadata) => {
if (!isBidirectional(attribute) || isOwner(attribute)) {
createJoinTable(metadata, {
attribute,
attributeName,
meta,
});
}
};
const relationFactoryMap = {
oneToOne: createOneToOne,
oneToMany: createOneToMany,
manyToOne: createManyToOne,
manyToMany: createManyToMany,
};
const hasInversedBy = _.has('inversedBy');
const hasMappedBy = _.has('mappedBy');
const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(attribute);
const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute);
const shouldUseJoinTable = attribute => attribute.useJoinTable !== false;
const createJoinColum = (metadata, { attribute /*attributeName, meta */ }) => {
const targetMeta = metadata.get(attribute.target);
const joinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
const joinColumn = {
name: joinColumnName,
referencedColumn: 'id',
referencedTable: targetMeta.tableName,
};
Object.assign(attribute, { owner: true, joinColumn });
if (isBidirectional(attribute)) {
const inverseAttribute = targetMeta.attributes[attribute.inversedBy];
// TODO: do not invert here but invert in the query ? => means we need to use owner info in the query layer
Object.assign(inverseAttribute, {
joinColumn: {
name: joinColumn.referencedColumn,
referencedColumn: joinColumn.name,
},
});
}
};
const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
const targetMeta = metadata.get(attribute.target);
if (!targetMeta) {
throw new Error(`Unknow target ${attribute.target}`);
}
const joinTableName = _.snakeCase(`${meta.tableName}_${attributeName}_links`);
const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
const inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
metadata.add({
uid: joinTableName,
tableName: joinTableName,
attributes: {
[joinColumnName]: {
type: 'integer',
column: {
unsigned: true,
},
},
[inverseJoinColumnName]: {
type: 'integer',
column: {
unsigned: true,
},
},
// TODO: add extra pivot attributes -> user should use an intermediate entity
},
foreignKeys: [
{
name: `${joinTableName}_fk`,
columns: [joinColumnName],
referencedColumns: ['id'],
referencedTable: meta.tableName,
onDelete: 'CASCADE',
},
{
name: `${joinTableName}_inv_fk`,
columns: [inverseJoinColumnName],
referencedColumns: ['id'],
referencedTable: targetMeta.tableName,
onDelete: 'CASCADE',
},
],
});
const joinTable = {
name: joinTableName,
joinColumn: {
name: joinColumnName,
referencedColumn: 'id',
},
inverseJoinColumn: {
name: inverseJoinColumnName,
referencedColumn: 'id',
},
};
attribute.joinTable = joinTable;
if (isBidirectional(attribute)) {
const inverseAttribute = targetMeta.attributes[attribute.inversedBy];
if (!inverseAttribute) {
throw new Error(`inversedBy attribute ${attribute.inversedBy} not found target`);
}
inverseAttribute[joinTable] = {
name: joinTableName,
joinColumn: joinTable.inverseJoinColumn,
inverseJoinColumn: joinTable.joinColumn,
};
}
};
module.exports = {
createRelation,
};

View File

@ -49,7 +49,6 @@ const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
return subAlias;
};
// TODO: cleanup & implement real joins
const createJoin = (ctx, { alias, attributeName, attribute }) => {
const { db, qb } = ctx;
@ -59,8 +58,8 @@ const createJoin = (ctx, { alias, attributeName, attribute }) => {
const tragetMeta = db.metadata.get(attribute.target);
// TODO: inmplement joinColumn
const joinColumn = attribute.joinColumn;
if (joinColumn) {
const subAlias = qb.getAlias();
qb.join({

View File

@ -8,9 +8,7 @@ const createQueryBuilder = (uid, db) => {
const meta = db.metadata.get(uid);
const { tableName } = meta;
// TODO: we could use a state to track the entire query instead of using knex directly
let state = {
const state = {
type: 'select',
select: [],
count: null,
@ -28,9 +26,6 @@ const createQueryBuilder = (uid, db) => {
let counter = 0;
const getAlias = () => `t${counter++}`;
// TODO: actually rename columns to attributes then pick them
// const pickAttributes = _.pick(Object.keys(meta.attributes));
return {
alias: getAlias(),
getAlias,
@ -62,7 +57,6 @@ const createQueryBuilder = (uid, db) => {
return this;
},
// TODO: convert where into aliases where & nested joins
where(where = {}) {
const processedWhere = helpers.processWhere(where, { qb: this, uid, db });
@ -71,7 +65,6 @@ const createQueryBuilder = (uid, db) => {
return this;
},
// TODO: handle aliasing logic
select(args) {
state.type = 'select';
state.select = _.castArray(args).map(col => this.aliasColumn(col));
@ -94,7 +87,6 @@ const createQueryBuilder = (uid, db) => {
return this;
},
// TODO: map to column name
orderBy(orderBy) {
state.orderBy = helpers.processOrderBy(orderBy, { qb: this, uid, db });
return this;
@ -106,10 +98,6 @@ const createQueryBuilder = (uid, db) => {
return this;
},
// TODO: implement
having() {},
// TODO: add necessary joins to make populate easier / faster
populate(populate) {
state.populate = helpers.processPopulate(populate, { qb: this, uid, db });
@ -239,7 +227,6 @@ const createQueryBuilder = (uid, db) => {
helpers.applyWhere(qb, state.where);
}
// TODO: apply joins
if (state.joins.length > 0) {
helpers.applyJoins(qb, state.joins);
}