Skip to content

Commit 4e9e1c0

Browse files
Wojciech KrysiakWojciech Krysiak
Wojciech Krysiak
authored and
Wojciech Krysiak
committed
feat: support delete and update hooks
closes SoftwareBrothers/adminjs#503
1 parent dc00273 commit 4e9e1c0

File tree

5 files changed

+119
-52
lines changed

5 files changed

+119
-52
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = {
5252
files: ['./src/**/*.spec.ts', 'spec/*.ts'],
5353
rules: {
5454
'no-unused-expressions': 'off',
55+
'prefer-arrow-callback': 'off',
5556
'func-names': 'off',
5657
'import/no-extraneous-dependencies': 'off',
5758
},

example-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@admin-bro/express": "^3.0.0",
23-
"@admin-bro/sequelize": "../",
23+
"@admin-bro/sequelize": "1.0.0-beta.5",
2424
"admin-bro": "^3.1.1",
2525
"dotenv": "^8.2.0",
2626
"express": "^4.17.1",

example-app/yarn.lock

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
resolved "https://registry.yarnpkg.com/@admin-bro/express/-/express-3.0.0.tgz#0ac581bbd4511d8854d07409b980e0fff3d9d8e0"
1919
integrity sha512-Ya/a3F/JT//VanaIjlVBeU6gh1afwcjU/q9qhMcETKwy3M+QbVhfmLdOyD1mFqVxwifnkiGnC6gPkupb51C9Pg==
2020

21-
"@admin-bro/sequelize@../":
22-
version "1.0.0-beta.3"
21+
"@admin-bro/[email protected]":
22+
version "1.0.0-beta.5"
23+
resolved "https://registry.yarnpkg.com/@admin-bro/sequelize/-/sequelize-1.0.0-beta.5.tgz#662d875ad2fedd781003bb84d303d37be691e0ad"
24+
integrity sha512-MpXND4ZheWePaA2rpxhedpeUwzbI0OaLfyKvdpZif/SsFgsDOEzgto3QryLyMSkXtCdzXh54NYwKR9Uku0c0VQ==
2325
dependencies:
2426
escape-regexp "0.0.1"
2527
flat "^5.0.0"

src/resource.spec.ts

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,88 @@ import { ValidationError, Filter, BaseRecord } from 'admin-bro'
33

44
import chai, { expect } from 'chai'
55
import sinonChai from 'sinon-chai'
6+
import sinon from 'sinon'
67

7-
import Resource from './resource'
8+
import Resource, { ModelType } from './resource'
89
import Property from './property'
910

1011
chai.use(sinonChai)
1112

1213
const config = require('../config/config')[process.env.NODE_ENV as string]
1314
const db = require('../models/index.js')
1415

15-
describe('Resource', () => {
16+
describe('Resource', function () {
17+
let SequelizeModel: ModelType<any>
18+
let resource: Resource
19+
1620
before(function () {
17-
this.SequelizeModel = db.sequelize.models.User
18-
this.resource = new Resource(this.SequelizeModel)
21+
SequelizeModel = db.sequelize.models.User
22+
resource = new Resource(SequelizeModel)
1923
})
2024

2125
afterEach(async function () {
22-
await this.SequelizeModel.destroy({ where: {} })
26+
await SequelizeModel.destroy({ where: {} })
2327
})
2428

2529
describe('.isAdapterFor', () => {
2630
it('returns true when sequelize model is given', function () {
27-
expect(Resource.isAdapterFor(this.SequelizeModel)).to.equal(true)
31+
expect(Resource.isAdapterFor(SequelizeModel)).to.equal(true)
2832
})
2933
})
3034

3135
describe('#database', () => {
3236
it('returns correct database name', function () {
33-
expect(this.resource.databaseName()).to.equal(config.database)
37+
expect(resource.databaseName()).to.equal(config.database)
3438
})
3539
})
3640

3741
describe('#databaseType', () => {
3842
it('returns correct database', function () {
39-
expect(this.resource.databaseType()).to.equal(config.dialect)
43+
expect(resource.databaseType()).to.equal(config.dialect)
4044
})
4145
})
4246

4347
describe('#name', () => {
4448
it('returns correct name', function () {
45-
expect(this.resource.name()).to.equal('Users')
49+
expect(resource.name()).to.equal('Users')
4650
})
4751
})
4852

4953
describe('#id', () => {
5054
it('returns correct name', function () {
51-
expect(this.resource.id()).to.equal('Users')
55+
expect(resource.id()).to.equal('Users')
5256
})
5357
})
5458

5559
describe('#properties', () => {
5660
it('returns all properties', function () {
5761
const length = 8 // there are 8 properties in the User model (5 regular + __v and _id)
58-
expect(this.resource.properties()).to.have.lengthOf(length)
62+
expect(resource.properties()).to.have.lengthOf(length)
5963
})
6064
})
6165

6266
describe('#property', () => {
6367
it('returns given property', function () {
64-
expect(this.resource.property('email')).to.be.an.instanceOf(Property)
68+
expect(resource.property('email')).to.be.an.instanceOf(Property)
6569
})
6670

6771
it('returns null when property doesn\'t exit', function () {
68-
expect(this.resource.property('some.imagine.property')).to.be.null
72+
expect(resource.property('some.imagine.property')).to.be.null
6973
})
7074

7175
it('returns nested property for array field', function () {
72-
const property = this.resource.property('arrayed.1')
76+
const property = resource.property('arrayed.1')
7377

7478
expect(property).to.be.an.instanceOf(Property)
75-
expect(property.type()).to.equal('string')
79+
expect(property?.type()).to.equal('string')
7680
})
7781
})
7882

7983
describe('#findMany', () => {
8084
it('returns array of BaseRecords', async function () {
81-
const params = await this.resource.create({ email: '[email protected]' })
85+
const params = await resource.create({ email: '[email protected]' })
8286

83-
const records = await this.resource.findMany([params.id])
87+
const records = await resource.findMany([params.id])
8488

8589
expect(records).to.have.lengthOf(1)
8690
expect(records[0]).to.be.instanceOf(BaseRecord)
@@ -93,14 +97,14 @@ describe('Resource', () => {
9397
gender: 'male',
9498
9599
}
96-
this.record = await this.resource.create(this.params)
100+
this.record = await resource.create(this.params)
97101
})
98102

99103
it('returns 1 BaseRecord when filtering on ENUMS', async function () {
100104
const filter = new Filter({
101105
gender: 'male',
102-
}, this.resource)
103-
const records = await this.resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
106+
}, resource)
107+
const records = await resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
104108

105109
expect(records).to.have.lengthOf(1)
106110
expect(records[0]).to.be.instanceOf(BaseRecord)
@@ -110,18 +114,18 @@ describe('Resource', () => {
110114
it('returns 0 BaseRecord when filtering on ENUMS', async function () {
111115
const filter = new Filter({
112116
gender: 'female',
113-
}, this.resource)
114-
const records = await this.resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
117+
}, resource)
118+
const records = await resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
115119

116120
expect(records).to.have.lengthOf(0)
117121
})
118122

119123
it('returns error when filtering on ENUMS with invalid value', async function () {
120124
const filter = new Filter({
121125
gender: 'XXX',
122-
}, this.resource)
126+
}, resource)
123127
try {
124-
await this.resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
128+
await resource.find(filter, { limit: 20, offset: 0, sort: { direction: 'asc', sortBy: 'id' } })
125129
} catch (error) {
126130
expect(error.name).to.eq('SequelizeDatabaseError')
127131
}
@@ -130,30 +134,30 @@ describe('Resource', () => {
130134

131135
describe('#count', () => {
132136
it('returns 0 when there are none elements', async function () {
133-
const count = await this.resource.count(new Filter({} as any, {} as any))
137+
const count = await resource.count(new Filter({} as any, {} as any))
134138
expect(count).to.equal(0)
135139
})
136140

137141
it('returns given count without filters', async function () {
138-
await this.resource.create({ email: '[email protected]' })
139-
expect(await this.resource.count(new Filter({} as any, {} as any))).to.equal(1)
142+
await resource.create({ email: '[email protected]' })
143+
expect(await resource.count(new Filter({} as any, {} as any))).to.equal(1)
140144
})
141145

142146
it('returns given count for given filters', async function () {
143-
await this.resource.create({
147+
await resource.create({
144148
firstName: 'john',
145149
lastName: 'doe',
146150
147151
})
148-
await this.resource.create({
152+
await resource.create({
149153
firstName: 'andrew',
150154
lastName: 'golota',
151155
152156
})
153157
const filter = new Filter({
154158
155-
}, this.resource)
156-
expect(await this.resource.count(filter)).to.equal(1)
159+
}, resource)
160+
expect(await resource.count(filter)).to.equal(1)
157161
})
158162
})
159163

@@ -165,11 +169,11 @@ describe('Resource', () => {
165169
lastName: 'doe',
166170
167171
}
168-
this.record = await this.resource.create(this.params)
172+
this.record = await resource.create(this.params)
169173
})
170174

171175
it('creates new user when data is valid', async function () {
172-
const newCount = await this.resource.count()
176+
const newCount = await resource.count(null as any)
173177
expect(newCount).to.equal(1)
174178
})
175179

@@ -189,7 +193,7 @@ describe('Resource', () => {
189193

190194
it('throws validation error', async function () {
191195
try {
192-
await this.resource.create(this.params)
196+
await resource.create(this.params)
193197
} catch (error) {
194198
expect(error).to.be.an.instanceOf(ValidationError)
195199
}
@@ -198,8 +202,8 @@ describe('Resource', () => {
198202

199203
context('record with empty id field', () => {
200204
beforeEach(function () {
201-
this.SequelizeModel = db.sequelize.models.Post
202-
this.resource = new Resource(this.SequelizeModel)
205+
SequelizeModel = db.sequelize.models.Post
206+
resource = new Resource(SequelizeModel)
203207
})
204208

205209
it('creates record without an error', async function () {
@@ -209,28 +213,82 @@ describe('Resource', () => {
209213
publishedAt: '2019-12-10 12:00',
210214
userId: '',
211215
}
212-
this.recordParams = await this.resource.create(this.params)
216+
this.recordParams = await resource.create(this.params)
213217
expect(this.recordParams.userId).to.be.null
214218
})
215219
})
216220
})
217221

218222
describe('#update', () => {
219223
beforeEach(async function () {
224+
SequelizeModel = db.sequelize.models.User
225+
resource = new Resource(SequelizeModel)
220226
this.params = {
221227
firstName: 'john',
222228
lastName: 'doe',
223229
224230
}
225-
this.record = await this.resource.create(this.params)
231+
this.record = await resource.create(this.params)
226232
})
227233

228234
it('updates the title', async function () {
229235
this.newEmail = '[email protected]'
230-
this.record = await this.resource.update(this.record.id, {
236+
const params = await resource.update(this.record.id, {
231237
email: this.newEmail,
232238
})
233-
expect(this.record.email).to.equal(this.newEmail)
239+
240+
expect(params.email).to.equal(this.newEmail)
241+
})
242+
243+
it('calls update hooks', async function () {
244+
const beforeUpdateSpy = sinon.spy()
245+
const afterUpdateSpy = sinon.spy()
246+
const beforeBulkUpdateSpy = sinon.spy()
247+
SequelizeModel.addHook('beforeUpdate', beforeUpdateSpy)
248+
SequelizeModel.addHook('beforeBulkUpdate', beforeBulkUpdateSpy)
249+
SequelizeModel.addHook('afterUpdate', afterUpdateSpy)
250+
251+
await resource.update(this.record.id, { firstName: 'jack' })
252+
253+
expect(beforeUpdateSpy).to.have.been.called
254+
expect(afterUpdateSpy).to.have.been.called
255+
expect(beforeBulkUpdateSpy).not.to.have.been.called
256+
})
257+
})
258+
259+
describe('#delete', () => {
260+
beforeEach(async function () {
261+
SequelizeModel = db.sequelize.models.User
262+
resource = new Resource(SequelizeModel)
263+
this.params = {
264+
firstName: 'john',
265+
lastName: 'doe',
266+
267+
}
268+
this.record = await resource.create(this.params)
269+
})
270+
271+
it('deletes the resource', async function () {
272+
await resource.delete(this.record.id)
273+
274+
const newRecord = await resource.findOne(this.record.id)
275+
276+
expect(newRecord).to.be.null
277+
})
278+
279+
it('calls delete hooks', async function () {
280+
const beforeDestroySpy = sinon.spy()
281+
const afterDestroySpy = sinon.spy()
282+
const beforeDestroyUpdateSpy = sinon.spy()
283+
SequelizeModel.addHook('beforeDestroy', beforeDestroySpy)
284+
SequelizeModel.addHook('beforeBulkDestroy', beforeDestroyUpdateSpy)
285+
SequelizeModel.addHook('afterDestroy', afterDestroySpy)
286+
287+
await resource.delete(this.record.id)
288+
289+
expect(beforeDestroySpy).to.have.been.called
290+
expect(afterDestroySpy).to.have.been.called
291+
expect(beforeDestroyUpdateSpy).not.to.have.been.called
234292
})
235293
})
236294
})

src/resource.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const SEQUELIZE_UNIQUE_ERROR = 'SequelizeUniqueConstraintError'
1414
// this fixes problem with unbound this when you setup type of Mode as a member of another
1515
// class: https://stackoverflow.com/questions/55166230/sequelize-typescript-typeof-model
1616
type Constructor<T> = new (...args: any[]) => T;
17-
type ModelType<T extends Model<T>> = Constructor<T> & typeof Model;
17+
export type ModelType<T extends Model<T>> = Constructor<T> & typeof Model;
1818

1919
type FindOptions = {
2020
limit?: number;
@@ -127,8 +127,11 @@ class Resource extends BaseResource {
127127
return sequelizeObjects.map((sequelizeObject) => new BaseRecord(sequelizeObject.toJSON(), this))
128128
}
129129

130-
async findOne(id) {
130+
async findOne(id): Promise<BaseRecord | null> {
131131
const sequelizeObject = await this.findById(id)
132+
if (!sequelizeObject) {
133+
return null
134+
}
132135
return new BaseRecord(sequelizeObject.toJSON(), this)
133136
}
134137

@@ -148,7 +151,7 @@ class Resource extends BaseResource {
148151
return this.SequelizeModel[method](id)
149152
}
150153

151-
async create(params) {
154+
async create(params): Promise<Record<string, any>> {
152155
const parsedParams = this.parseParams(params)
153156
const unflattedParams = unflatten(parsedParams)
154157
try {
@@ -173,6 +176,8 @@ class Resource extends BaseResource {
173176
where: {
174177
[this.primaryKey()]: id,
175178
},
179+
individualHooks: true,
180+
hooks: false,
176181
})
177182
const record = await this.findById(id)
178183
return record.toJSON()
@@ -187,12 +192,13 @@ class Resource extends BaseResource {
187192
}
188193
}
189194

190-
async delete(id) {
191-
this.SequelizeModel.destroy({
192-
where: {
193-
[this.primaryKey()]: id,
194-
},
195-
})
195+
async delete(id): Promise<void> {
196+
// we find first because we need to invoke destroy on model, so all hooks
197+
// instance hooks (not bulk) are called.
198+
// We cannot set {individualHooks: true, hooks: false} in this.SequelizeModel.destroy,
199+
// as it is in #update method because for some reason it wont delete the record
200+
const model = await this.SequelizeModel.findByPk(id)
201+
await model.destroy()
196202
}
197203

198204
/**

0 commit comments

Comments
 (0)