Skip to content

Commit 713f360

Browse files
authored
Merge pull request #36 from maikmb/master
feat(custom-convention): add custom convention to repository
2 parents a46434a + 6744ef8 commit 713f360

File tree

9 files changed

+288
-38
lines changed

9 files changed

+288
-38
lines changed

.vscode/launch.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"999999",
1515
"--colors",
1616
"--recursive",
17-
"${workspaceFolder}/test"
17+
"${workspaceFolder}/test/unit"
1818
],
1919
"internalConsoleOptions": "openOnSessionStart",
2020
"skipFiles": [
@@ -32,7 +32,7 @@
3232
"999999",
3333
"--colors",
3434
"--recursive",
35-
"${workspaceFolder}/testdb/mssql"
35+
"${workspaceFolder}/test/integration/mssql"
3636
],
3737
"internalConsoleOptions": "openOnSessionStart",
3838
"skipFiles": [
@@ -51,7 +51,7 @@
5151
"999999",
5252
"--colors",
5353
"--recursive",
54-
"${workspaceFolder}/testdb/mysql"
54+
"${workspaceFolder}/test/integration/mysql"
5555
],
5656
"internalConsoleOptions": "openOnSessionStart",
5757
"skipFiles": [
@@ -70,7 +70,7 @@
7070
"999999",
7171
"--colors",
7272
"--recursive",
73-
"${workspaceFolder}/testdb/pg"
73+
"${workspaceFolder}/test/integration/pg"
7474
],
7575
"internalConsoleOptions": "openOnSessionStart",
7676
"skipFiles": [

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,35 @@ const repo = new ItemRepository(injection)
252252
const ret = await repo.delete(entity)
253253
```
254254

255-
### Conventions - Defaul implementation
255+
## Conventions
256+
257+
### Default implementation
256258

257259
#### Fields
258260

259261
Code: Camel Case - ex: `productName`
260262

261263
Database: Snake Case - ex: `product_name`
262264

265+
### Custom Convention
266+
267+
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.
268+
269+
```javascript
270+
const toCamelCase = value => camelCase(value)
271+
272+
const userRepository = new UserRepository({
273+
entity: User,
274+
table,
275+
schema,
276+
ids: ['id'],
277+
knex: connection,
278+
convention: {
279+
toTableFieldName: field => toCamelCase(field)
280+
}
281+
})
282+
```
283+
263284
### Object-Oriented versus Relational models - Relationships
264285

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

302323
Features:
303-
- [ ] Be able to change the conventions (injection)
324+
- [X] Be able to change the conventions (injection)
304325
- [ ] Exclude / ignore fields on a sql statement
305326
- [ ] Awareness of created/updated at/by fields
306327
- [X] Plug-and-play knex

src/convention.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
module.exports = class Convention {
2-
3-
static camelToSnake(string) {
4-
return string.replace(/([A-Z])/g, "_$1").toLowerCase()
5-
}
6-
7-
static toTableFieldName(entityField) {
8-
return this.camelToSnake(entityField)
9-
}
10-
11-
static isScalarType(type) {
12-
const scalarTypes = [Number, String, Boolean, Date, Object, Array]
13-
return scalarTypes.includes(type)
14-
}
15-
16-
}
2+
camelToSnake (string) {
3+
return string.replace(/([A-Z])/g, '_$1').toLowerCase()
4+
}
5+
6+
toTableFieldName (entityField) {
7+
return this.camelToSnake(entityField)
8+
}
9+
10+
isScalarType (type) {
11+
const scalarTypes = [Number, String, Boolean, Date, Object, Array]
12+
return scalarTypes.includes(type)
13+
}
14+
}

src/dataMapper.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
const Convention = require('./convention')
22
const { entity } = require('@herbsjs/gotu')
3-
const dependency = { convention: Convention }
43

54
class DataMapper {
6-
75
constructor(entity, entityIDs = [], foreignKeys = [], options = {}) {
8-
const di = Object.assign({}, dependency, options.injection)
9-
this.convention = di.convention
6+
this.convention = Object.assign(new Convention(), options.convention)
107
this.entity = entity
118
const schema = entity.prototype.meta.schema
129
this.allFields = DataMapper.buildAllFields(schema, entityIDs, foreignKeys, this.convention)

src/repository.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
const Convention = require("./convention")
2-
const DataMapper = require("./dataMapper")
1+
const Convention = require('./convention')
2+
const DataMapper = require('./dataMapper')
33
const { checker } = require('@herbsjs/suma')
44

5-
const dependency = { convention: Convention }
6-
75
module.exports = class Repository {
86
constructor(options) {
9-
const di = Object.assign({}, dependency, options.injection)
10-
this.convention = di.convention
7+
this.convention = Object.assign(new Convention(), options.convention)
118
this.table = options.table
129
this.schema = options.schema
1310
this.tableQualifiedName = this.schema
@@ -17,7 +14,12 @@ module.exports = class Repository {
1714
this.entityIDs = options.ids
1815
this.foreignKeys = options.foreignKeys
1916
this.knex = options.knex
20-
this.dataMapper = new DataMapper(this.entity, this.entityIDs, this.foreignKeys)
17+
this.dataMapper = new DataMapper(
18+
this.entity,
19+
this.entityIDs,
20+
this.foreignKeys,
21+
options
22+
)
2123
}
2224

2325
runner() {
@@ -232,7 +234,4 @@ module.exports = class Repository {
232234

233235

234236

235-
}
236-
237-
238-
237+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
const { entity, field } = require('@herbsjs/gotu')
2+
const Repository = require('../../../src/repository')
3+
const db = require('./db')
4+
const connection = require('../connection')
5+
const assert = require('assert')
6+
const { camelCase } = require('lodash')
7+
8+
describe('Query Find with Conventions', () => {
9+
const table = 'testRepository'
10+
const schema = 'herbs2knex_testdb'
11+
12+
const givenAnRepositoryClass = options => {
13+
return class ItemRepositoryBase extends Repository {
14+
constructor () {
15+
super(options)
16+
}
17+
}
18+
}
19+
20+
const GivenAnEntity = () => {
21+
return entity('A entity', {
22+
id: field(Number),
23+
stringTest: field(String),
24+
booleanTest: field(Boolean)
25+
})
26+
}
27+
28+
const GivenAnSnakeCaseEntity = () => {
29+
return entity('A entity', {
30+
id: field(Number),
31+
string_test: field(String),
32+
boolean_test: field(Boolean)
33+
})
34+
}
35+
36+
before(async () => {
37+
const sql = `
38+
DROP SCHEMA IF EXISTS ${schema} CASCADE;
39+
CREATE SCHEMA ${schema};
40+
DROP TABLE IF EXISTS ${schema}.${table} CASCADE;
41+
CREATE TABLE ${schema}."${table}" (
42+
"id" INT,
43+
"stringTest" TEXT,
44+
"booleanTest" BOOL
45+
)`
46+
await db.query(sql)
47+
48+
await db.query(
49+
`INSERT INTO ${schema}."${table}" ("id", "stringTest", "booleanTest") VALUES (10, 'marie', true)`
50+
)
51+
})
52+
53+
after(async () => {
54+
const sql = `
55+
DROP SCHEMA IF EXISTS ${schema} CASCADE;
56+
`
57+
await db.query(sql)
58+
})
59+
60+
it('should return entities using table field custom convention for camel case', async () => {
61+
//given
62+
const toCamelCase = value => camelCase(value)
63+
64+
const anEntity = GivenAnEntity()
65+
const ItemRepository = givenAnRepositoryClass({
66+
entity: anEntity,
67+
table,
68+
schema,
69+
ids: ['id'],
70+
knex: connection,
71+
convention: {
72+
toTableFieldName: field => toCamelCase(field)
73+
}
74+
})
75+
76+
const itemRepo = new ItemRepository()
77+
78+
//when
79+
const ret = await itemRepo.find({ where: { stringTest: ['marie'] } })
80+
81+
//then
82+
assert.deepStrictEqual(ret[0].toJSON(), {
83+
id: 10,
84+
stringTest: 'marie',
85+
booleanTest: true
86+
})
87+
})
88+
89+
it("should return entities when the convention of the entity's fields is different from the database", async () => {
90+
//given
91+
const toCamelCase = value => camelCase(value)
92+
93+
const anEntity = GivenAnSnakeCaseEntity()
94+
const ItemRepository = givenAnRepositoryClass({
95+
entity: anEntity,
96+
table,
97+
schema,
98+
ids: ['id'],
99+
knex: connection,
100+
convention: {
101+
toTableFieldName: field => toCamelCase(field)
102+
}
103+
})
104+
105+
const itemRepo = new ItemRepository()
106+
107+
//when
108+
const ret = await itemRepo.find({ where: { stringTest: ['marie'] } })
109+
110+
//then
111+
assert.deepStrictEqual(ret[0].toJSON(), {
112+
id: 10,
113+
string_test: 'marie',
114+
boolean_test: true
115+
})
116+
})
117+
118+
it('should return a error when custom convention throws a exception', async () => {
119+
//given
120+
const anEntity = GivenAnSnakeCaseEntity()
121+
const ItemRepository = givenAnRepositoryClass({
122+
entity: anEntity,
123+
table,
124+
schema,
125+
ids: ['id'],
126+
knex: connection,
127+
convention: {
128+
toTableFieldName: _ => {
129+
throw new Error('error')
130+
}
131+
}
132+
})
133+
134+
135+
//when & then
136+
assert.throws(() => new ItemRepository())
137+
})
138+
})

test/unit/convention.test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ describe('Convetion', () => {
66
it('entity field name to database field name', () => {
77
//given
88
const entityField = "fieldName"
9+
const convention = new Convetion()
910
//when
10-
const dbField = Convetion.toTableFieldName(entityField)
11+
const dbField = convention.toTableFieldName(entityField)
1112
//then
1213
assert.deepStrictEqual(dbField, "field_name")
1314
})
15+
1416
})

0 commit comments

Comments
 (0)