Skip to content

Commit

Permalink
feat: add support for timestamps in many to many relationship
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Apr 26, 2021
1 parent 8a22b85 commit 36b7e4a
Show file tree
Hide file tree
Showing 8 changed files with 594 additions and 18 deletions.
5 changes: 5 additions & 0 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ declare module '@ioc:Adonis/Lucid/Model' {
ExcutableQueryBuilderContract<Result[]> {
model: Model

/**
* Define a callback to transform a row
*/
rowTransformer(callback: (row: LucidRow) => void): this

/**
* Define a custom preloader for the current query
*/
Expand Down
6 changes: 6 additions & 0 deletions adonis-typings/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ declare module '@ioc:Adonis/Lucid/Relations' {
relatedKey?: string
pivotRelatedForeignKey?: string
pivotColumns?: string[]
pivotTimestamps?:
| boolean
| {
createdAt: string | boolean
updatedAt: string | boolean
}
serializeAs?: string | null
onQuery?(query: Related['builder'] | Related['subQuery']): void
}
Expand Down
44 changes: 38 additions & 6 deletions src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
TransactionClientContract,
} from '@ioc:Adonis/Lucid/Database'

import { isObject } from '../../utils'
import { Preloader } from '../Preloader'
import { QueryRunner } from '../../QueryRunner'
import { Chainable } from '../../Database/QueryBuilder/Chainable'
Expand Down Expand Up @@ -78,6 +79,11 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
*/
protected preloader: PreloaderContract<LucidRow> = new Preloader(this.model)

/**
* A custom callback to transform each model row
*/
protected rowTransformerCallback: (row: LucidRow) => void

/**
* Required by macroable
*/
Expand Down Expand Up @@ -169,13 +175,27 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
}

/**
* Convert fetch results to an array of model instances
* Convert fetched results to an array of model instances
*/
const modelInstances = this.model.$createMultipleFromAdapterResult(
rows,
this.sideloaded,
this.clientOptions
)
const modelInstances = rows.reduce((models: LucidRow[], row: ModelObject) => {
if (isObject(row)) {
const modelInstance = this.model.$createFromAdapterResult(
row,
this.sideloaded,
this.clientOptions
)!

/**
* Transform row when row transformer is defined
*/
if (this.rowTransformerCallback) {
this.rowTransformerCallback(modelInstance)
}

models.push(modelInstance)
}
return models
}, [])

/**
* Preload for model instances
Expand Down Expand Up @@ -314,13 +334,22 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
return this
}

/**
* Define a custom callback to transform rows
*/
public rowTransformer(callback: (row: LucidRow) => void): this {
this.rowTransformerCallback = callback
return this
}

/**
* Clone the current query builder
*/
public clone(): ModelQueryBuilder {
const clonedQuery = new ModelQueryBuilder(this.knexQuery.clone(), this.model, this.client)
this.applyQueryFlags(clonedQuery)
clonedQuery.sideloaded = Object.assign({}, this.sideloaded)
this.rowTransformerCallback && this.rowTransformer(this.rowTransformerCallback)
return clonedQuery
}

Expand Down Expand Up @@ -372,6 +401,9 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
return this
}

/**
* Define a custom preloader instance for preloading relationships
*/
public usePreloader(preloader: PreloaderContract<LucidRow>) {
this.preloader = preloader
return this
Expand Down
43 changes: 40 additions & 3 deletions src/Orm/Relations/ManyToMany/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { Knex } from 'knex'
import { DateTime } from 'luxon'
import { LucidModel, LucidRow } from '@ioc:Adonis/Lucid/Model'
import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
import { ManyToManyQueryBuilderContract } from '@ioc:Adonis/Lucid/Relations'
Expand Down Expand Up @@ -177,9 +178,9 @@ export class ManyToManyQueryBuilder
* Select columns from the pivot table
*/
this.pivotColumns(
[this.relation.pivotForeignKey, this.relation.pivotRelatedForeignKey].concat(
this.relation.pivotColumns
)
[this.relation.pivotForeignKey, this.relation.pivotRelatedForeignKey]
.concat(this.relation.pivotColumns)
.concat(this.relation.pivotTimestamps)
)
}

Expand Down Expand Up @@ -387,6 +388,42 @@ export class ManyToManyQueryBuilder
return super.paginate(page, perPage)
}

public async exec() {
const pivotTimestamps = this.relation.pivotTimestamps.map((timestamp) =>
this.relation.pivotAlias(timestamp)
)

/**
* Transform pivot timestamps
*/
if (pivotTimestamps.length) {
this.rowTransformer((row) => {
pivotTimestamps.forEach((timestamp) => {
const timestampValue = row.$extras[timestamp]
if (!timestampValue) {
return
}

/**
* Convert from string
*/
if (typeof timestampValue === 'string') {
row.$extras[timestamp] = DateTime.fromSQL(timestampValue)
}

/**
* Convert from date
*/
if (timestampValue instanceof Date) {
row.$extras[timestamp] = DateTime.fromJSDate(timestampValue)
}
})
})
}

return super.exec()
}

/**
* Returns the group limit query
*/
Expand Down
31 changes: 24 additions & 7 deletions src/Orm/Relations/ManyToMany/QueryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* file that was distributed with this source code.
*/

import { DateTime } from 'luxon'
import { OneOrMany } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder'
import { ManyToManyClientContract } from '@ioc:Adonis/Lucid/Relations'
import { LucidModel, LucidRow, ModelObject } from '@ioc:Adonis/Lucid/Model'
Expand Down Expand Up @@ -50,6 +51,27 @@ export class ManyToManyQueryClient implements ManyToManyClientContract<ManyToMan
private client: QueryClientContract
) {}

/**
* Returns the timestamps for the pivot row
*/
private getPivotTimestamps(updatedAtOnly: boolean) {
const timestamps: ModelObject = {}

if (this.relation.pivotCreatedAtTimestamp && !updatedAtOnly) {
timestamps[this.relation.pivotCreatedAtTimestamp] = DateTime.local().toFormat(
this.client.dialect.dateTimeFormat
)
}

if (this.relation.pivotUpdatedAtTimestamp) {
timestamps[this.relation.pivotUpdatedAtTimestamp] = DateTime.local().toFormat(
this.client.dialect.dateTimeFormat
)
}

return timestamps
}

/**
* Generate a related query builder
*/
Expand Down Expand Up @@ -285,7 +307,7 @@ export class ManyToManyQueryClient implements ManyToManyClientContract<ManyToMan
*/
const pivotRows = (!hasAttributes ? (ids as (string | number)[]) : Object.keys(ids)).map(
(id) => {
return Object.assign({}, hasAttributes ? ids[id] : {}, {
return Object.assign(this.getPivotTimestamps(false), hasAttributes ? ids[id] : {}, {
[this.relation.pivotForeignKey]: foreignKeyValue,
[this.relation.pivotRelatedForeignKey]: id,
})
Expand Down Expand Up @@ -389,8 +411,6 @@ export class ManyToManyQueryClient implements ManyToManyClientContract<ManyToMan
pivotRows
)

console.log({ added, updated })

/**
* Add new rows
*/
Expand All @@ -401,14 +421,11 @@ export class ManyToManyQueryClient implements ManyToManyClientContract<ManyToMan
*/
for (let id of Object.keys(updated)) {
const attributes = updated[id]
if (!attributes) {
return Promise.resolve()
}

await this.pivotQuery()
.useTransaction(transaction)
.wherePivot(this.relation.pivotRelatedForeignKey, id)
.update(attributes)
.update(Object.assign({}, this.getPivotTimestamps(true), attributes))
}

/**
Expand Down
63 changes: 63 additions & 0 deletions src/Orm/Relations/ManyToMany/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,67 @@ export class ManyToMany implements ManyToManyRelationContract<LucidModel, LucidM
public pivotTable: string
public pivotColumns: string[] = this.options.pivotColumns || []

public pivotCreatedAtTimestamp: string | undefined
public pivotUpdatedAtTimestamp: string | undefined

/**
* Timestamp columns for the pivot table
*/
public get pivotTimestamps(): string[] {
const timestamps: string[] = []
this.pivotCreatedAtTimestamp && timestamps.push(this.pivotCreatedAtTimestamp)
this.pivotUpdatedAtTimestamp && timestamps.push(this.pivotUpdatedAtTimestamp)

return timestamps
}

/**
* Reference to the onQuery hook defined by the user
*/
public onQueryHook = this.options.onQuery

/**
* Computes the created at timestamps column name
* for the pivot table
*/
private computedCreatedAtTimestamp() {
if (!this.options.pivotTimestamps) {
return
}

if (this.options.pivotTimestamps === true) {
this.pivotCreatedAtTimestamp = 'created_at'
return
}

if (typeof this.options.pivotTimestamps.createdAt === 'string') {
this.pivotCreatedAtTimestamp = this.options.pivotTimestamps.createdAt
} else if (this.options.pivotTimestamps.createdAt === true) {
this.pivotCreatedAtTimestamp = 'created_at'
}
}

/**
* Computes the updated at timestamps column name
* for the pivot table
*/
private computedUpdatedAtTimestamp() {
if (!this.options.pivotTimestamps) {
return
}

if (this.options.pivotTimestamps === true) {
this.pivotUpdatedAtTimestamp = 'updated_at'
return
}

if (typeof this.options.pivotTimestamps.updatedAt === 'string') {
this.pivotUpdatedAtTimestamp = this.options.pivotTimestamps.updatedAt
} else if (this.options.pivotTimestamps.updatedAt === true) {
this.pivotUpdatedAtTimestamp = 'updated_at'
}
}

constructor(
public relationName: string,
public relatedModel: () => LucidModel,
Expand Down Expand Up @@ -150,6 +206,13 @@ export class ManyToMany implements ManyToManyRelationContract<LucidModel, LucidM
this.relationName
)

/**
* Configure pivot timestamps to use. This is a temporary
* setup. We will have to soon introduce pivot models
*/
this.computedCreatedAtTimestamp()
this.computedUpdatedAtTimestamp()

/**
* Booted successfully
*/
Expand Down
4 changes: 2 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ export function syncDiff(original: ModelObject, incoming: ModelObject) {
* the upcoming row
*/
if (!originalRow) {
result.added[incomingRowId] = incoming[incomingRowId]
result.added[incomingRowId] = incomingRow
} else if (Object.keys(incomingRow).find((key) => incomingRow[key] !== originalRow[key])) {
/**
* If any of the row attributes are different, then we must
* update that row
*/
result.updated[incomingRowId] = incoming[incomingRowId]
result.updated[incomingRowId] = incomingRow
}

return result
Expand Down
Loading

0 comments on commit 36b7e4a

Please sign in to comment.