Skip to content

Commit

Permalink
feat: include custom convention to read database fields
Browse files Browse the repository at this point in the history
  • Loading branch information
maikvortx committed Jan 2, 2022
1 parent 22c5260 commit 6ea3118
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 115 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,35 @@ const repo = new ItemRepository(injection)
const ret = await repo.delete(entity)
```

### Conventions - Defaul implementation
## Conventions

### Defaul implementation

#### Fields

Code: Camel Case - ex: `productName`

Database: Snake Case - ex: `product_name`

### Custom Convention

You can use the custom convention to configure the way herbs2knex creates your queries to read fields from your database. When using this option, the `ids` property must respect the format convention.

```javascript
const toCamelCase = value => camelCase(value)
const ItemRepository = UserRepository({
entity: anEntity,
table,
schema,
ids: ['id'],
knex: connection,
convention: {
toTableFieldName: field => toCamelCase(field)
}
})
```

### Object-Oriented versus Relational models - Relationships

An entity can define a reference for others entities but will not (and should not) define a foreign key. For instance:
Expand Down Expand Up @@ -300,7 +321,7 @@ More about: https://en.wikipedia.org/wiki/Object%E2%80%93relational_impedance_mi
- [ ] Allow to ommit schema's name

Features:
- [ ] Be able to change the conventions (injection)
- [X] Be able to change the conventions (injection)
- [ ] Exclude / ignore fields on a sql statement
- [ ] Awareness of created/updated at/by fields
- [X] Plug-and-play knex
Expand Down
2 changes: 1 addition & 1 deletion src/dataMapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class DataMapper {
this.convention = di.convention
this.entity = entity
const schema = entity.prototype.meta.schema
this.allFields = DataMapper.buildAllFields(schema, entityIDs, foreignKeys, this.convention)
this.allFields = DataMapper.buildAllFields(schema, entityIDs, foreignKeys, this.convention)
this._proxy === undefined
}

Expand Down
241 changes: 129 additions & 112 deletions src/repository.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
const Convention = require("./convention")
const DataMapper = require("./dataMapper")
const Convention = require('./convention')
const DataMapper = require('./dataMapper')
const { checker } = require('@herbsjs/suma')

const dependency = { convention: Convention }

module.exports = class Repository {
constructor(options) {
const di = Object.assign({}, dependency, options.injection)
this.convention = di.convention
constructor (options) {
this.convention = Object.assign(dependency.convention, options.convention)
this.table = options.table
this.schema = options.schema
this.tableQualifiedName = this.schema
Expand All @@ -17,21 +16,26 @@ module.exports = class Repository {
this.entityIDs = options.ids
this.foreignKeys = options.foreignKeys
this.knex = options.knex
this.dataMapper = new DataMapper(this.entity, this.entityIDs, this.foreignKeys)
this.dataMapper = new DataMapper(
this.entity,
this.entityIDs,
this.foreignKeys,
options
)
}

runner() {
runner () {
return this.knex(this.tableQualifiedName)
}

/**
*
* Finds entities matching the ID condition.
*
* @param {type} ids The id or the array of id's to search
* @return {type} List of entities
*/
async findByID(ids) {
/**
*
* Finds entities matching the ID condition.
*
* @param {type} ids The id or the array of id's to search
* @return {type} List of entities
*/
async findByID (ids) {
const tableIDs = this.dataMapper.tableIDs()
const tableFields = this.dataMapper.tableFields()

Expand All @@ -50,28 +54,39 @@ module.exports = class Repository {
return entities
}

async #executeFindQuery(query, options) {


async #executeFindQuery (query, options) {
if (options.where) {
const conditionTermTableField = this.dataMapper.toTableFieldName(Object.keys(options.where)[0])
const conditionTermTableField = this.dataMapper.toTableFieldName(
Object.keys(options.where)[0]
)
const conditionTerm = Object.keys(options.where)[0]
if (!conditionTerm || conditionTerm === "0") throw "condition term is invalid"
if (!conditionTerm || conditionTerm === '0')
throw 'condition term is invalid'

const conditionValue = Array.isArray(options.where[conditionTerm])
? options.where[conditionTerm]
: [options.where[conditionTerm]]

if (!options.where[conditionTerm] ||
(typeof options.where[conditionTerm] === "object" && !Array.isArray(options.where[conditionTerm])) ||
(Array.isArray(options.where[conditionTerm]) && !options.where[conditionTerm].length))
throw "condition value is invalid"
if (
!options.where[conditionTerm] ||
(typeof options.where[conditionTerm] === 'object' &&
!Array.isArray(options.where[conditionTerm])) ||
(Array.isArray(options.where[conditionTerm]) &&
!options.where[conditionTerm].length)
)
throw 'condition value is invalid'

query = query.whereIn(conditionTermTableField, conditionValue)
}

if (options.orderBy) {
if (!options.orderBy || typeof options.orderBy === "object" && !Array.isArray(options.orderBy) && checker.isEmpty(options.orderBy)) throw "order by is invalid"
if (
!options.orderBy ||
(typeof options.orderBy === 'object' &&
!Array.isArray(options.orderBy) &&
checker.isEmpty(options.orderBy))
)
throw 'order by is invalid'
query = query.orderBy(options.orderBy)
}

Expand All @@ -84,80 +99,84 @@ module.exports = class Repository {
if (row === undefined) continue
entities.push(this.dataMapper.toEntity(row))
}
}
else
entities.push(this.dataMapper.toEntity(ret))
} else entities.push(this.dataMapper.toEntity(ret))
}
return entities
}

/**
*
* Finds entities matching the conditions.
*
* @param {type} object.limit Limit items to list
* @param {type} object.offset Rows that will be skipped from the resultset
* @param {type} object.where Where query term
* @param {type} object.orderBy Order by query
*
* @return {type} List of entities
*/
async find(options = {
limit: 0,
offset: 0,
orderBy: null,
where: null
}) {

/**
*
* Finds entities matching the conditions.
*
* @param {type} object.limit Limit items to list
* @param {type} object.offset Rows that will be skipped from the resultset
* @param {type} object.where Where query term
* @param {type} object.orderBy Order by query
*
* @return {type} List of entities
*/
async find (
options = {
limit: 0,
offset: 0,
orderBy: null,
where: null
}
) {
options.orderBy = options.orderBy || null
options.limit = options.limit || 0
options.offset = options.offset || 0
options.where = options.where || null

const tableFields = this.dataMapper.tableFields()

let query = this.runner()
.select(tableFields)
let query = this.runner().select(tableFields)

if (options.limit > 0) query = query.limit(options.limit)
if (options.offset > 0) query = query.offset(options.offset)

return this.#executeFindQuery(query, options)
}

/**
*
* Find all entities
*
* @param {type} object.limit Limit items to list
* @param {type} object.orderBy Order by query
* @param {type} object.offset Rows that will be skipped from the resultset
*
* @return {type} List of entities
*/
async findAll(options = {
limit: 0,
offset: 0,
orderBy: null
}) {

const entities = this.find({ limit: options.limit, offset: options.offset, orderBy: options.orderBy })
/**
*
* Find all entities
*
* @param {type} object.limit Limit items to list
* @param {type} object.orderBy Order by query
* @param {type} object.offset Rows that will be skipped from the resultset
*
* @return {type} List of entities
*/
async findAll (
options = {
limit: 0,
offset: 0,
orderBy: null
}
) {
const entities = this.find({
limit: options.limit,
offset: options.offset,
orderBy: options.orderBy
})
return entities
}

/**
*
* Finds the first entity matching the conditions.
*
* @param {type} object.orderBy Order by query to get the first element of, if null will return the first element without order
*
* @return {type} Entity
*/
async first(options = {
orderBy: null,
where: null
}) {

/**
*
* Finds the first entity matching the conditions.
*
* @param {type} object.orderBy Order by query to get the first element of, if null will return the first element without order
*
* @return {type} Entity
*/
async first (
options = {
orderBy: null,
where: null
}
) {
options.orderBy = options.orderBy || null
options.where = options.where || null

Expand All @@ -168,15 +187,15 @@ module.exports = class Repository {
return this.#executeFindQuery(query, options)
}

/**
*
* Create a new entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} Current entity
*/
async insert(entityInstance) {
/**
*
* Create a new entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} Current entity
*/
async insert (entityInstance) {
const fields = this.dataMapper.tableFields()
const payload = this.dataMapper.tableFieldsWithValue(entityInstance)

Expand All @@ -187,15 +206,15 @@ module.exports = class Repository {
return this.dataMapper.toEntity(ret[0])
}

/**
*
* Update entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} Current entity
*/
async update(entityInstance) {
/**
*
* Update entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} Current entity
*/
async update (entityInstance) {
const tableIDs = this.dataMapper.tableIDs()
const fields = this.dataMapper.tableFields()
const payload = this.dataMapper.tableFieldsWithValue(entityInstance)
Expand All @@ -206,21 +225,25 @@ module.exports = class Repository {
.update(payload)

//.returning() is not supported by mysql or mysql2 and will not have any effect, update only return 1 to true or 0 to false
if(this.runner().client && this.runner().client.driverName && this.runner().client.driverName.includes('mysql'))
if (
this.runner().client &&
this.runner().client.driverName &&
this.runner().client.driverName.includes('mysql')
)
return ret === 1

return this.dataMapper.toEntity(ret[0])
}

/**
*
* Delete entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} True when success
*/
async delete(entityInstance) {
/**
*
* Delete entity
*
* @param {type} entityInstance Entity instance
*
* @return {type} True when success
*/
async delete (entityInstance) {
const tableIDs = this.dataMapper.tableIDs()

const ret = await this.runner()
Expand All @@ -229,10 +252,4 @@ module.exports = class Repository {

return ret === 1
}



}



Loading

0 comments on commit 6ea3118

Please sign in to comment.