2021-06-17 16:17:15 +02:00
'use strict' ;
2021-06-22 17:13:11 +02:00
const _ = require ( 'lodash/fp' ) ;
const types = require ( './types' ) ;
2021-06-28 12:34:29 +02:00
const { createField } = require ( './fields' ) ;
2021-06-17 16:17:15 +02:00
const { createQueryBuilder } = require ( './query' ) ;
const { createRepository } = require ( './entity-repository' ) ;
2021-06-28 21:37:44 +02:00
// TODO: move to query layer
const toRow = ( metadata , data = { } ) => {
2021-06-22 17:13:11 +02:00
const { attributes } = metadata ;
2021-06-23 15:37:20 +02:00
const obj = { } ;
2021-06-22 17:13:11 +02:00
2021-06-23 15:37:20 +02:00
for ( const attributeName in attributes ) {
const attribute = attributes [ attributeName ] ;
if ( types . isScalar ( attribute . type ) && _ . has ( attributeName , data ) ) {
2021-06-28 12:34:29 +02:00
// TODO: we convert to column name
2021-06-30 20:00:03 +02:00
// TODO: handle default value
2021-06-28 12:34:29 +02:00
const field = createField ( attribute . type , attribute ) ;
2021-06-28 21:37:44 +02:00
// TODO: validate data on creation
2021-06-28 12:34:29 +02:00
// field.validate(data[attributeName]);
2021-06-30 20:00:03 +02:00
2021-06-28 21:37:44 +02:00
const val = data [ attributeName ] === null ? null : field . toDB ( data [ attributeName ] ) ;
2021-06-28 12:34:29 +02:00
2021-06-28 21:37:44 +02:00
obj [ attributeName ] = val ;
2021-06-23 15:37:20 +02:00
}
if ( types . isRelation ( attribute . type ) ) {
// oneToOne & manyToOne
if ( attribute . joinColumn && attribute . owner ) {
// TODO: ensure joinColumn name respect convention ?
const joinColumnName = attribute . joinColumn . name ;
2021-06-25 12:07:32 +02:00
// allow setting to null
const attrValue = ! _ . isUndefined ( data [ attributeName ] )
? data [ attributeName ]
: data [ joinColumnName ] ;
2021-06-23 15:37:20 +02:00
if ( ! _ . isUndefined ( attrValue ) ) {
obj [ joinColumnName ] = attrValue ;
}
}
}
}
return obj ;
} ;
2021-06-17 16:17:15 +02:00
const createEntityManager = db => {
const repoMap = { } ;
return {
async findOne ( uid , params ) {
const qb = this . createQueryBuilder ( uid )
. init ( params )
. first ( ) ;
return await qb . execute ( ) ;
} ,
// should we name it findOne because people are used to it ?
async findMany ( uid , params ) {
const qb = this . createQueryBuilder ( uid ) . init ( params ) ;
return await qb . execute ( ) ;
} ,
// support search directly in find & count -> a search param ? a different feature with a search tables rather
async findWithCount ( uid , params ) {
return await Promise . all ( [ this . findMany ( uid , params ) , this . count ( uid , params ) ] ) ;
} ,
2021-06-22 17:13:11 +02:00
async count ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
const qb = this . createQueryBuilder ( uid ) . where ( params . where ) ;
const res = await qb
. count ( )
. first ( )
. execute ( ) ;
return Number ( res . count ) ;
} ,
2021-06-25 12:07:32 +02:00
async create ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
// create entry in DB
2021-06-23 15:37:20 +02:00
const metadata = db . metadata . get ( uid ) ;
2021-06-17 16:17:15 +02:00
const { data } = params ;
2021-06-25 12:07:32 +02:00
if ( ! _ . isPlainObject ( data ) ) {
throw new Error ( 'Create expects a data object' ) ;
}
2021-06-17 16:17:15 +02:00
// 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
2021-06-23 15:37:20 +02:00
2021-06-28 21:37:44 +02:00
const dataToInsert = toRow ( metadata , data ) ;
2021-06-17 16:17:15 +02:00
2021-06-25 12:07:32 +02:00
// if (_.isEmpty(dataToInsert)) {
// throw new Error('Create requires data');
// }
2021-06-22 17:13:11 +02:00
2021-06-17 16:17:15 +02:00
const [ id ] = await this . createQueryBuilder ( uid )
2021-06-22 17:13:11 +02:00
. insert ( dataToInsert )
2021-06-17 16:17:15 +02:00
. execute ( ) ;
// create relation associations or move this to the entity service & call attach on the repo instead
2021-06-28 12:34:29 +02:00
await this . attachRelations ( metadata , id , data ) ;
2021-06-17 16:17:15 +02:00
2021-06-25 12:07:32 +02:00
// TODO: in case there is not select or populate specified return the inserted data ?
2021-06-17 16:17:15 +02:00
return this . findOne ( uid , { where : { id } , select : params . select , populate : params . populate } ) ;
} ,
2021-06-25 12:07:32 +02:00
// TODO: where do we handle relation processing for many queries ?
async createMany ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
const { data } = params ;
2021-06-25 12:07:32 +02:00
if ( ! _ . isArray ( data ) ) {
throw new Error ( 'CreateMany expecets data to be an array' ) ;
}
2021-06-23 15:37:20 +02:00
const metadata = db . metadata . get ( uid ) ;
2021-06-28 21:37:44 +02:00
const dataToInsert = data . map ( datum => toRow ( metadata , datum ) ) ;
2021-06-22 17:13:11 +02:00
2021-06-24 18:28:36 +02:00
if ( _ . isEmpty ( dataToInsert ) ) {
2021-06-25 12:07:32 +02:00
throw new Error ( 'Nothing to insert' ) ;
2021-06-24 18:28:36 +02:00
}
await this . createQueryBuilder ( uid )
2021-06-22 17:13:11 +02:00
. insert ( dataToInsert )
2021-06-17 16:17:15 +02:00
. execute ( ) ;
2021-06-24 18:28:36 +02:00
return { count : data . length } ;
2021-06-17 16:17:15 +02:00
} ,
2021-06-25 12:07:32 +02:00
async update ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
const { where , data } = params ;
2021-06-23 15:37:20 +02:00
const metadata = db . metadata . get ( uid ) ;
2021-06-22 17:13:11 +02:00
2021-06-30 20:00:03 +02:00
if ( ! _ . isPlainObject ( data ) ) {
throw new Error ( 'Update requires a data object' ) ;
}
2021-06-25 12:07:32 +02:00
if ( _ . isEmpty ( where ) ) {
throw new Error ( 'Update requires a where parameter' ) ;
2021-06-24 18:28:36 +02:00
}
2021-06-25 12:07:32 +02:00
const entity = await this . createQueryBuilder ( uid )
. select ( 'id' )
2021-06-17 16:17:15 +02:00
. where ( where )
2021-06-25 12:07:32 +02:00
. first ( )
2021-06-17 16:17:15 +02:00
. execute ( ) ;
2021-06-25 12:07:32 +02:00
if ( ! entity ) {
// TODO: or throw ?
return null ;
}
const { id } = entity ;
2021-06-28 21:37:44 +02:00
const dataToUpdate = toRow ( metadata , data ) ;
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
if ( ! _ . isEmpty ( dataToUpdate ) ) {
await this . createQueryBuilder ( uid )
. where ( { id } )
. update ( dataToUpdate )
. execute ( ) ;
}
2021-06-28 12:34:29 +02:00
await this . updateRelations ( metadata , id , data ) ;
2021-06-25 12:07:32 +02:00
return this . findOne ( uid , { where : { id } , select : params . select , populate : params . populate } ) ;
2021-06-17 16:17:15 +02:00
} ,
2021-06-25 12:07:32 +02:00
// TODO: where do we handle relation processing for many queries ?
async updateMany ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
const { where , data } = params ;
2021-06-23 15:37:20 +02:00
const metadata = db . metadata . get ( uid ) ;
2021-06-28 21:37:44 +02:00
const dataToUpdate = toRow ( metadata , data ) ;
2021-06-22 17:13:11 +02:00
2021-06-24 18:28:36 +02:00
if ( _ . isEmpty ( dataToUpdate ) ) {
throw new Error ( 'Update requires data' ) ;
}
2021-06-25 12:07:32 +02:00
const updatedRows = await this . createQueryBuilder ( uid )
2021-06-17 16:17:15 +02:00
. where ( where )
2021-06-22 17:13:11 +02:00
. update ( dataToUpdate )
2021-06-17 16:17:15 +02:00
. execute ( ) ;
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
return { count : updatedRows } ;
} ,
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
async delete ( uid , params = { } ) {
const { where , select , populate } = params ;
const metadata = db . metadata . get ( uid ) ;
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
if ( _ . isEmpty ( where ) ) {
throw new Error ( 'Delete requires a where parameter' ) ;
}
2021-06-17 16:17:15 +02:00
2021-06-25 12:07:32 +02:00
const entity = await this . findOne ( uid , {
where ,
select : select && [ 'id' ] . concat ( select ) ,
populate ,
} ) ;
if ( ! entity ) {
return null ;
}
const { id } = entity ;
await this . createQueryBuilder ( uid )
. where ( { id } )
2021-06-17 16:17:15 +02:00
. delete ( )
. execute ( ) ;
2021-06-24 18:28:36 +02:00
2021-06-28 12:34:29 +02:00
await this . deleteRelations ( metadata , id ) ;
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
return entity ;
2021-06-17 16:17:15 +02:00
} ,
2021-06-25 12:07:32 +02:00
// TODO: where do we handle relation processing for many queries ?
async deleteMany ( uid , params = { } ) {
2021-06-17 16:17:15 +02:00
const { where } = params ;
2021-06-25 12:07:32 +02:00
const deletedRows = await this . createQueryBuilder ( uid )
2021-06-17 16:17:15 +02:00
. where ( where )
. delete ( )
. execute ( ) ;
2021-06-24 18:28:36 +02:00
2021-06-25 12:07:32 +02:00
return { count : deletedRows } ;
2021-06-17 16:17:15 +02:00
} ,
// populate already loaded entry
async populate ( uid , entry , name , params ) {
return {
... entry ,
relation : await this . load ( entry , name , params ) ,
} ;
} ,
// loads a relation
load ( uid , entry , name , params ) {
const { attributes } = db . metadata . get ( uid ) ;
return this . getRepository ( attributes [ name ] . target . uid ) . findMany ( {
... params ,
where : {
... params . where ,
// [parent]: entry.id,
} ,
} ) ;
} ,
2021-06-28 12:34:29 +02:00
/ * *
* Attach relations to a new entity
2021-06-30 21:17:32 +02:00
*
2021-06-28 12:34:29 +02:00
* @ param { EntityManager } em - entity manager instance
* @ param { Metadata } metadata - model metadta
* @ param { ID } id - entity ID
* @ param { object } data - data received for creation
* /
async attachRelations ( metadata , id , data ) {
const { attributes } = metadata ;
for ( const attributeName in attributes ) {
const attribute = attributes [ attributeName ] ;
if ( attribute . joinColumn && attribute . owner ) {
// nothing to do => relation already added on the table
continue ;
}
// oneToOne oneToMany on the non owning side
if ( attribute . joinColumn && ! attribute . owner ) {
// need to set the column on the target
const { target } = attribute ;
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
if ( data [ attributeName ] ) {
await this . 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 ) {
// need to set the column on the target
const { joinTable } = attribute ;
const { joinColumn , inverseJoinColumn } = joinTable ;
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
if ( data [ attributeName ] ) {
const insert = _ . castArray ( data [ attributeName ] ) . map ( datum => {
return {
[ joinColumn . name ] : id ,
[ inverseJoinColumn . name ] : datum ,
... ( joinTable . on || { } ) ,
} ;
} ) ;
// if there is nothing to insert
if ( insert . length === 0 ) {
return ;
}
await this . createQueryBuilder ( joinTable . name )
. insert ( insert )
. execute ( ) ;
}
}
}
} ,
/ * *
* Updates relations of an existing entity
2021-06-30 21:17:32 +02:00
*
2021-06-28 12:34:29 +02:00
* @ 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)
async updateRelations ( 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 this . 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 this . createQueryBuilder ( joinTable . name )
. delete ( )
. where ( { [ joinColumn . name ] : id } )
. where ( joinTable . on ? joinTable . on : { } )
. execute ( ) ;
const insert = _ . castArray ( data [ attributeName ] ) . map ( datum => {
return {
[ joinColumn . name ] : id ,
[ inverseJoinColumn . name ] : datum ,
... ( joinTable . on || { } ) ,
} ;
} ) ;
// if there is nothing to insert
if ( insert . length === 0 ) {
return ;
}
await this . 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
2021-06-28 21:37:44 +02:00
* NOTE : Most of the deletion should be handled by ON DELETE CASCADE for dialect that have FKs
2021-06-30 21:17:32 +02:00
*
2021-06-28 12:34:29 +02:00
* @ param { EntityManager } em - entity manager instance
* @ param { Metadata } metadata - model metadta
* @ param { ID } id - entity ID
* /
async deleteRelations ( metadata , id ) {
2021-06-28 21:37:44 +02:00
// TODO: Implement correctly
if ( db . dialect . usesForeignKeys ( ) ) {
return ;
}
2021-06-28 12:34:29 +02:00
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 ;
await this . createQueryBuilder ( target )
. where ( { [ attribute . joinColumn . referencedColumn ] : id } )
. delete ( )
. execute ( ) ;
}
if ( attribute . joinTable ) {
const { joinTable } = attribute ;
const { joinColumn } = joinTable ;
await this . createQueryBuilder ( joinTable . name )
. delete ( )
. where ( { [ joinColumn . name ] : id } )
. where ( joinTable . on ? joinTable . on : { } )
. execute ( ) ;
}
}
} ,
2021-06-17 16:17:15 +02:00
// cascading
// aggregations
// -> avg
// -> min
// -> max
// -> grouping
// formulas
// custom queries
// utilities
// -> format
// -> parse
// -> map result
// -> map input
// -> validation
// extra features
// -> virtuals
// -> private
2021-06-30 21:17:32 +02:00
2021-06-17 16:17:15 +02:00
createQueryBuilder ( uid ) {
return createQueryBuilder ( uid , db ) ;
} ,
getRepository ( uid ) {
if ( ! repoMap [ uid ] ) {
repoMap [ uid ] = createRepository ( uid , db ) ;
}
return repoMap [ uid ] ;
} ,
clearRepositories ( ) {
repoMap . clear ( ) ;
} ,
} ;
} ;
module . exports = {
createEntityManager ,
} ;