Skip to content

Commit

Permalink
Merge pull request #71 from Zooip/custom-attributes-equality
Browse files Browse the repository at this point in the history
Add dirtyChecker to Attributes definitions
  • Loading branch information
wadetandy authored Aug 2, 2020
2 parents 8ac8fb6 + f28c044 commit 5b04ba1
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 14 deletions.
11 changes: 11 additions & 0 deletions src/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ export type Attr<T> = (() => T) | { new (...args: any[]): T & object }

export type AttrType<T> = Attr<T>

export type DirtyChecker<T> = (prior: T, current: T) => boolean

export interface AttrRecord<T> {
name?: string
type?: AttrType<T>
persist?: boolean
dirtyChecker?: DirtyChecker<T>
}

export const attr = <T = any>(options?: AttrRecord<T>): Attribute<T> => {
Expand All @@ -26,13 +29,17 @@ export type AttributeOptions = Partial<{
name: string
type: () => any
persist: boolean
dirtyChecker?: DirtyChecker<any>
}>

export const STRICT_EQUALITY_DIRTY_CHECKER: DirtyChecker<any> = (prior, current) => (prior !== current)

export class Attribute<T = any> {
isRelationship = false
name!: string
type?: T = undefined
persist: boolean = true
dirtyChecker: DirtyChecker<T> = STRICT_EQUALITY_DIRTY_CHECKER
owner!: typeof SpraypaintBase

constructor(options: AttrRecord<T>) {
Expand All @@ -51,6 +58,10 @@ export class Attribute<T = any> {
if (options.persist !== undefined) {
this.persist = !!options.persist
}

if (options.dirtyChecker) {
this.dirtyChecker = (options.dirtyChecker)
}
}

apply(ModelClass: typeof SpraypaintBase): void {
Expand Down
3 changes: 2 additions & 1 deletion src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,8 @@ export class SpraypaintBase {
Object.keys(attrs).forEach(k => {
let self = this as any
let changes = this.changes() as any
if (self[k] !== attrs[k] && !changes[k]) {
let attrDef = this.klass.attributeList[k]
if (attrDef.dirtyChecker(self[k], attrs[k]) && !changes[k]) {
diff[k] = [self[k], attrs[k]]
self[k] = attrs[k]

Expand Down
10 changes: 5 additions & 5 deletions src/util/dirty-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SpraypaintBase, ModelRecord, ModelAttributeChangeSet } from "../model"
import { IncludeDirective, IncludeScopeHash } from "./include-directive"
import { IncludeScope } from "../scope"
import { JsonapiResourceIdentifier } from "../jsonapi-spec"
import {isEmpty} from "lodash";

class DirtyChecker<T extends SpraypaintBase> {
model: T
Expand Down Expand Up @@ -58,9 +59,11 @@ class DirtyChecker<T extends SpraypaintBase> {
const prior = (<any>this.model)._originalAttributes[key]
const current = this.model.attributes[key]

let attrDef = this.model.klass.attributeList[key]

if (!this.model.isPersisted) {
dirty[key] = [null, current]
} else if (prior !== current) {
} else if (attrDef.dirtyChecker(prior, current)) {
dirty[key] = [prior, current]
}
}
Expand All @@ -77,10 +80,7 @@ class DirtyChecker<T extends SpraypaintBase> {
}

private _hasDirtyAttributes() {
const originalAttrs = (<any>this.model)._originalAttributes
const currentAttrs = this.model.attributes

return JSON.stringify(originalAttrs) !== JSON.stringify(currentAttrs)
return !isEmpty(this.dirtyAttributes())
}

private _hasDirtyRelationships(includeHash: IncludeScopeHash): boolean {
Expand Down
13 changes: 12 additions & 1 deletion test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
hasOne
} from "../src/index"

import { Attr, BelongsTo, HasMany, HasOne, Link } from "../src/decorators"
import {Attr, BelongsTo, HasMany, HasOne, Link} from "../src/decorators"
import {DirtyChecker} from "../src/attribute";

@Model({
baseUrl: "http://example.com",
Expand Down Expand Up @@ -40,11 +41,21 @@ export class PersonWithLinks extends Person {
@Link() webView!: string
}

export interface Coordinates{
lon: number;
lat: number;
}

const dirtyCoordinatesChecker : DirtyChecker<Coordinates> = (prior: Coordinates, current: Coordinates) => (
(prior.lon !== current.lon) || (prior.lat !== current.lat)
)

@Model()
export class PersonDetail extends ApplicationRecord {
static jsonapiType = "person_details"

@Attr address!: string
@Attr({dirtyChecker: dirtyCoordinatesChecker}) coordinates!: Coordinates | null
}

@Model({ keyCase: { server: "snake", client: "snake" } })
Expand Down
45 changes: 39 additions & 6 deletions test/integration/dirty-tracking.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { sinon, expect, fetchMock } from "../test-helper"
import { Person, Author, Book } from "../fixtures"
import {Person, Author, Book, PersonDetail} from "../fixtures"
import { IResultProxy } from "../../src/proxies/index"

// This is a Vue-specific test. Since isPersisted is already true,
// Vue will prevent the setter from firing. We cannot rely on
// side-effect behavior of model.isPersisted = true
// So, ensure we at least call reset() explicitly
describe("Dirty tracking", () => {
let responsePayload = (firstName: string) => {
let responsePayload = (type: string, attributes: object) => {
return {
data: {
id: "1",
type: "people",
attributes: { firstName }
attributes: { attributes }
}
}
}
Expand All @@ -22,9 +22,17 @@ describe("Dirty tracking", () => {
})

beforeEach(() => {
let url = "http://example.com/api/v1/authors"
fetchMock.post(url, responsePayload("John"))
fetchMock.patch(`${url}/1`, responsePayload("Jake"))
let url = "http://example.com/api"
fetchMock.post(`${url}/v1/authors`, responsePayload("people",{firstName: "John"}))
fetchMock.patch(`${url}/v1/authors/1`, responsePayload("people",{firstName: "Jake"}))

fetchMock.post(`${url}/person_details`, responsePayload("person_detail",{
address: "157 My Street, London, England",
coordinates: {
lon : 3,
lat : 48,
}
}))
})

describe("when persisted, dirty, updated", () => {
Expand All @@ -41,4 +49,29 @@ describe("Dirty tracking", () => {
expect(spy.callCount).to.eq(2)
})
})

describe("when custom dirty checker", () => {
it("handle custom checker", async () => {
let instance = new PersonDetail({
address: "157 My Street, London, England"
})
instance.coordinates = {
lon : 3,
lat : 48,
}
await instance.save()
expect(instance.isPersisted).to.eq(true)
console.log(instance.changes())
expect(instance.isDirty()).to.eq(false)

instance.coordinates.lon = 4
expect(instance.isDirty()).to.eq(true)

instance.coordinates = {
lon : 3,
lat : 48,
}
expect(instance.isDirty()).to.eq(false)
})
})
})
13 changes: 12 additions & 1 deletion test/unit/attributes.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, sinon } from "../test-helper"
import { attr, Attribute } from "../../src/attribute"
import { attr, Attribute, STRICT_EQUALITY_DIRTY_CHECKER } from "../../src/attribute"

describe("Attributes", () => {
describe("Initializing Attribute", () => {
Expand Down Expand Up @@ -33,5 +33,16 @@ describe("Attributes", () => {
const defaultAttr = attr({ persist: false })
expect(defaultAttr.persist).to.be.false
})

it( "defaults to strict equality", () => {
const defaultAttr = attr()
expect(defaultAttr.dirtyChecker).to.eq(STRICT_EQUALITY_DIRTY_CHECKER)
})

it( "allows dirty checker function to be overridden", () => {
const customChecker = (prior:any, current:any) => (prior != current)
const defaultAttr = attr({dirtyChecker: customChecker})
expect(defaultAttr.dirtyChecker).to.eq(customChecker)
})
})
})

0 comments on commit 5b04ba1

Please sign in to comment.