Skip to content

Commit 8b754c4

Browse files
authored
Merge pull request #394 from formidablejs/feature/relationships
Feature/relationships
2 parents 012f4b5 + 535c6c3 commit 8b754c4

File tree

4 files changed

+351
-2
lines changed

4 files changed

+351
-2
lines changed

src/Database/Database.imba

Lines changed: 306 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { attachPaginate } from 'knex-paginate'
33
import querystring from 'querystring'
44
import location from '../Support/Helpers/location'
55
import isString from '../Support/Helpers/isString'
6+
import singularize from '../Support/Helpers/singularize'
67
import Config from './Config'
78
import knex from 'knex'
89

@@ -37,13 +38,16 @@ try
3738
for column in columns
3839
object[column] = result[column]
3940
mappedResults.push(object)
40-
return mappedResults
41+
results = mappedResults
4142

4243
if this._hidden && Array.isArray(this._hidden) && this._hidden.length > 0
4344
for result in results
4445
for column in this._hidden
4546
delete result[column]
4647

48+
if this._relationships && Array.isArray(this._relationships) && this._relationships.length > 0
49+
results = await this._loadRelationships(results)
50+
4751
results
4852

4953
knex.QueryBuilder.extend 'autoPaginate', do(pageSize = 20)
@@ -109,6 +113,9 @@ try
109113
for column in this._hidden
110114
delete result[column]
111115

116+
if this._relationships && Array.isArray(this._relationships) && this._relationships.length > 0
117+
data = await this._loadRelationships(data)
118+
112119
const results = {
113120
data,
114121
pagination: {
@@ -190,6 +197,304 @@ try
190197

191198
return this
192199

200+
knex.QueryBuilder.extend 'belongsTo', do(relatedTable, queryCallback, foreignKey, localKey)
201+
this._relationships = this._relationships || []
202+
203+
let tableName = relatedTable
204+
if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName
205+
tableName = new relatedTable().tableName
206+
207+
if typeof queryCallback == 'function'
208+
this._relationships.push({
209+
type: 'belongsTo',
210+
relatedTable: tableName,
211+
queryCallback: queryCallback,
212+
foreignKey: foreignKey || singularize(tableName) + '_id',
213+
localKey: localKey || 'id'
214+
})
215+
else if typeof queryCallback == 'string'
216+
this._relationships.push({
217+
type: 'belongsTo',
218+
relatedTable: tableName,
219+
queryCallback: null,
220+
foreignKey: queryCallback,
221+
localKey: foreignKey || 'id'
222+
})
223+
else
224+
this._relationships.push({
225+
type: 'belongsTo',
226+
relatedTable: tableName,
227+
queryCallback: null,
228+
foreignKey: singularize(tableName) + '_id',
229+
localKey: 'id'
230+
})
231+
232+
return this
233+
234+
knex.QueryBuilder.extend 'hasOne', do(relatedTable, queryCallback, foreignKey, localKey)
235+
this._relationships = this._relationships || []
236+
237+
let tableName = relatedTable
238+
if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName
239+
tableName = new relatedTable().tableName
240+
241+
if typeof queryCallback == 'function'
242+
this._relationships.push({
243+
type: 'hasOne',
244+
relatedTable: tableName,
245+
queryCallback: queryCallback,
246+
foreignKey: foreignKey || singularize(this._single.table) + '_id',
247+
localKey: localKey || 'id'
248+
})
249+
else if typeof queryCallback == 'string'
250+
this._relationships.push({
251+
type: 'hasOne',
252+
relatedTable: tableName,
253+
queryCallback: null,
254+
foreignKey: queryCallback,
255+
localKey: foreignKey || 'id'
256+
})
257+
else
258+
this._relationships.push({
259+
type: 'hasOne',
260+
relatedTable: tableName,
261+
queryCallback: null,
262+
foreignKey: singularize(this._single.table) + '_id',
263+
localKey: 'id'
264+
})
265+
266+
return this
267+
268+
knex.QueryBuilder.extend 'hasMany', do(relatedTable, queryCallback, foreignKey, localKey)
269+
this._relationships = this._relationships || []
270+
271+
let tableName = relatedTable
272+
if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName
273+
tableName = new relatedTable().tableName
274+
275+
if typeof queryCallback == 'function'
276+
this._relationships.push({
277+
type: 'hasMany',
278+
relatedTable: tableName,
279+
queryCallback: queryCallback,
280+
foreignKey: foreignKey || singularize(this._single.table) + '_id',
281+
localKey: localKey || 'id'
282+
})
283+
else if typeof queryCallback == 'string'
284+
this._relationships.push({
285+
type: 'hasMany',
286+
relatedTable: tableName,
287+
queryCallback: null,
288+
foreignKey: queryCallback,
289+
localKey: foreignKey || 'id'
290+
})
291+
else
292+
this._relationships.push({
293+
type: 'hasMany',
294+
relatedTable: tableName,
295+
queryCallback: null,
296+
foreignKey: singularize(this._single.table) + '_id',
297+
localKey: 'id'
298+
})
299+
300+
return this
301+
302+
knex.QueryBuilder.extend 'belongsToMany', do(relatedTable, queryCallback, pivotTable, foreignKey, relatedKey, localKey, relatedLocalKey)
303+
this._relationships = this._relationships || []
304+
305+
let tableName = relatedTable
306+
if typeof relatedTable == 'function' && relatedTable.prototype && relatedTable.prototype.tableName
307+
tableName = new relatedTable().tableName
308+
309+
if typeof queryCallback == 'function'
310+
this._relationships.push({
311+
type: 'belongsToMany',
312+
relatedTable: tableName,
313+
queryCallback: queryCallback,
314+
pivotTable: pivotTable || singularize(this._single.table) + '_' + tableName,
315+
foreignKey: foreignKey || singularize(this._single.table) + '_id',
316+
relatedKey: relatedKey || singularize(tableName) + '_id',
317+
localKey: localKey || 'id',
318+
relatedLocalKey: relatedLocalKey || 'id'
319+
})
320+
else if typeof queryCallback == 'string'
321+
this._relationships.push({
322+
type: 'belongsToMany',
323+
relatedTable: tableName,
324+
queryCallback: null,
325+
pivotTable: queryCallback,
326+
foreignKey: foreignKey || singularize(this._single.table) + '_id',
327+
relatedKey: relatedKey || singularize(tableName) + '_id',
328+
localKey: localKey || 'id',
329+
relatedLocalKey: relatedLocalKey || 'id'
330+
})
331+
else
332+
this._relationships.push({
333+
type: 'belongsToMany',
334+
relatedTable: tableName,
335+
queryCallback: null,
336+
pivotTable: singularize(this._single.table) + '_' + tableName,
337+
foreignKey: singularize(this._single.table) + '_id',
338+
relatedKey: singularize(tableName) + '_id',
339+
localKey: 'id',
340+
relatedLocalKey: 'id'
341+
})
342+
343+
return this
344+
345+
knex.QueryBuilder.extend '_loadRelationships', do(results)
346+
if !results || results.length == 0
347+
return results
348+
349+
for relationship in this._relationships
350+
let relationshipName = relationship.relatedTable
351+
if relationship.type == 'belongsTo' || relationship.type == 'hasOne'
352+
relationshipName = singularize(relationship.relatedTable)
353+
354+
if relationship.type == 'belongsTo'
355+
await this._loadBelongsTo(results, relationship, relationshipName)
356+
else if relationship.type == 'hasOne'
357+
await this._loadHasOne(results, relationship, relationshipName)
358+
else if relationship.type == 'hasMany'
359+
await this._loadHasMany(results, relationship, relationshipName)
360+
else if relationship.type == 'belongsToMany'
361+
await this._loadBelongsToMany(results, relationship, relationshipName)
362+
363+
results
364+
365+
knex.QueryBuilder.extend '_loadBelongsTo', do(results, relationship, relationshipName)
366+
const hasForeignKey = results.length > 0 && results[0].hasOwnProperty(relationship.foreignKey)
367+
368+
if !hasForeignKey
369+
const ids = results.map(do(result) result.id).filter(do(id) id != null)
370+
371+
if ids.length == 0
372+
for result in results
373+
result[relationshipName] = null
374+
return
375+
376+
const foreignKeyResults = await Database(this._single.table)
377+
.select('id', relationship.foreignKey)
378+
.whereIn('id', ids)
379+
380+
const foreignKeyMap = {}
381+
for row in foreignKeyResults
382+
foreignKeyMap[row.id] = row[relationship.foreignKey]
383+
384+
for result in results
385+
result[relationship.foreignKey] = foreignKeyMap[result.id]
386+
387+
const foreignKeys = results.map(do(result) result[relationship.foreignKey]).filter(do(key) key != null)
388+
389+
if foreignKeys.length == 0
390+
for result in results
391+
result[relationshipName] = null
392+
return
393+
394+
let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.localKey, foreignKeys)
395+
396+
if relationship.queryCallback
397+
relatedQuery = relationship.queryCallback(relatedQuery)
398+
399+
const relatedResults = await relatedQuery
400+
const relatedMap = {}
401+
402+
for related in relatedResults
403+
relatedMap[related[relationship.localKey]] = related
404+
405+
for result in results
406+
result[relationshipName] = relatedMap[result[relationship.foreignKey]] || null
407+
408+
knex.QueryBuilder.extend '_loadHasOne', do(results, relationship, relationshipName)
409+
const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null)
410+
411+
if localKeys.length == 0
412+
for result in results
413+
result[relationshipName] = null
414+
return
415+
416+
let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.foreignKey, localKeys)
417+
418+
if relationship.queryCallback
419+
relatedQuery = relationship.queryCallback(relatedQuery)
420+
421+
const relatedResults = await relatedQuery
422+
const relatedMap = {}
423+
424+
for related in relatedResults
425+
relatedMap[related[relationship.foreignKey]] = related
426+
427+
for result in results
428+
result[relationshipName] = relatedMap[result[relationship.localKey]] || null
429+
430+
knex.QueryBuilder.extend '_loadHasMany', do(results, relationship, relationshipName)
431+
const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null)
432+
433+
if localKeys.length == 0
434+
for result in results
435+
result[relationshipName] = []
436+
return
437+
438+
let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.foreignKey, localKeys)
439+
440+
if relationship.queryCallback
441+
relatedQuery = relationship.queryCallback(relatedQuery)
442+
443+
const relatedResults = await relatedQuery
444+
const relatedMap = {}
445+
446+
for related in relatedResults
447+
const key = related[relationship.foreignKey]
448+
if !relatedMap[key]
449+
relatedMap[key] = []
450+
relatedMap[key].push(related)
451+
452+
for result in results
453+
result[relationshipName] = relatedMap[result[relationship.localKey]] || []
454+
455+
knex.QueryBuilder.extend '_loadBelongsToMany', do(results, relationship, relationshipName)
456+
const localKeys = results.map(do(result) result[relationship.localKey]).filter(do(key) key != null)
457+
458+
if localKeys.length == 0
459+
for result in results
460+
result[relationshipName] = []
461+
return
462+
463+
const pivotQuery = Database(relationship.pivotTable)
464+
.whereIn(relationship.foreignKey, localKeys)
465+
.select(relationship.foreignKey, relationship.relatedKey)
466+
467+
const pivotResults = await pivotQuery
468+
const pivotMap = {}
469+
470+
for pivot in pivotResults
471+
const key = pivot[relationship.foreignKey]
472+
if !pivotMap[key]
473+
pivotMap[key] = []
474+
pivotMap[key].push(pivot[relationship.relatedKey])
475+
476+
const allRelatedIds = [...new Set(pivotResults.map(do(pivot) pivot[relationship.relatedKey]))]
477+
478+
if allRelatedIds.length == 0
479+
for result in results
480+
result[relationshipName] = []
481+
return
482+
483+
let relatedQuery = Database(relationship.relatedTable).whereIn(relationship.relatedLocalKey, allRelatedIds)
484+
485+
if relationship.queryCallback
486+
relatedQuery = relationship.queryCallback(relatedQuery)
487+
488+
const relatedResults = await relatedQuery
489+
const relatedMap = {}
490+
491+
for related in relatedResults
492+
relatedMap[related[relationship.relatedLocalKey]] = related
493+
494+
for result in results
495+
const relatedIds = pivotMap[result[relationship.localKey]] || []
496+
result[relationshipName] = relatedIds.map(do(id) relatedMap[id]).filter(do(item) item != null)
497+
193498
knex.QueryBuilder.extend 'softDelete', do this.update({ deleted_at: Database.fn.now! })
194499

195500
knex.QueryBuilder.extend 'restore', do this.update({ deleted_at: null })

src/Database/Repository.imba

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,23 @@ export default class Repository
343343
const query = self.table!
344344

345345
query.get.apply(query, args)
346+
347+
static def belongsTo ...args
348+
const query = self.query!
349+
350+
query.belongsTo.apply(query, args)
351+
352+
static def hasOne ...args
353+
const query = self.query!
354+
355+
query.hasOne.apply(query, args)
356+
357+
static def hasMany ...args
358+
const query = self.query!
359+
360+
query.hasMany.apply(query, args)
361+
362+
static def belongsToMany ...args
363+
const query = self.query!
364+
365+
query.belongsToMany.apply(query, args)

types/Database/Database.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "knex";
22
import type { Knex } from "knex";
3+
import type Repository from "../Database/Repository";
34

45
declare let Database: Knex;
56
declare type Database = Knex;
@@ -52,7 +53,18 @@ declare module "knex" {
5253
*/
5354
autoPaginate<T = unknown>(perPage?: number): Promise<PaginationResults<T>>;
5455
hidden(columns: string[]): Knex.QueryBuilder;
55-
hasOne(related: string, foreignKey: string, localKey: string): Knex.QueryBuilder;
56+
belongsTo(related: string | typeof Repository): Knex.QueryBuilder;
57+
belongsTo(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder;
58+
belongsTo(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder;
59+
hasOne(related: string | typeof Repository): Knex.QueryBuilder;
60+
hasOne(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder;
61+
hasOne(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder;
62+
hasMany(related: string | typeof Repository): Knex.QueryBuilder;
63+
hasMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder;
64+
hasMany(related: string | typeof Repository, foreignKey: string, localKey: string): Knex.QueryBuilder;
65+
belongsToMany(related: string | typeof Repository): Knex.QueryBuilder;
66+
belongsToMany(related: string | typeof Repository, queryCallback: (query: Knex.QueryBuilder) => Knex.QueryBuilder): Knex.QueryBuilder;
67+
belongsToMany(related: string | typeof Repository, pivotTable: string, foreignKey?: string, relatedKey?: string, localKey?: string, relatedLocalKey?: string): Knex.QueryBuilder;
5668
}
5769
interface TableBuilder {
5870
softDeletes(): Knex.TableBuilder;

0 commit comments

Comments
 (0)