Skip to content

Commit fb490dc

Browse files
committed
feat: make restore act like save method
1 parent fa78920 commit fb490dc

5 files changed

Lines changed: 279 additions & 13 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/database",
3-
"version": "5.41.0",
3+
"version": "5.42.0",
44
"description": "The Athenna database handler for SQL/NoSQL.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/models/BaseModel.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,23 @@ export class BaseModel {
454454
return query.update(data, cleanPersist)
455455
}
456456

457+
/**
458+
* Restore a soft deleted value from database.
459+
*/
460+
public static async restore<T extends typeof BaseModel>(
461+
this: T,
462+
where: Partial<InstanceType<T>>,
463+
data: Partial<InstanceType<T>>
464+
): Promise<InstanceType<T> | InstanceType<T>[]> {
465+
const query = this.query()
466+
467+
if (where) {
468+
query.where(where)
469+
}
470+
471+
return query.restore(data)
472+
}
473+
457474
/**
458475
* Delete or soft delete a value in database.
459476
*/
@@ -758,14 +775,44 @@ export class BaseModel {
758775
*/
759776
public async restore() {
760777
const Model = this.constructor as any
761-
const primaryKey = Model.schema().getMainPrimaryKeyProperty()
778+
const schema = Model.schema()
779+
const primaryKey = schema.getMainPrimaryKeyProperty()
780+
const date = new Date()
781+
const createdAt = schema.getCreatedAtColumn()
782+
const updatedAt = schema.getUpdatedAtColumn()
783+
const deletedAt = schema.getDeletedAtColumn()
784+
const attributes = Model.isToSetAttributes ? Model.attributes() : {}
762785

763-
const restored = await Model.query()
764-
.where(primaryKey, this[primaryKey])
765-
.restore()
786+
Object.keys(attributes).forEach(key => {
787+
if (this[key]) {
788+
return
789+
}
790+
791+
this[key] = attributes[key]
792+
})
793+
794+
if (createdAt && this[createdAt.property] === undefined) {
795+
this[createdAt.property] = date
796+
}
797+
798+
if (updatedAt && this[updatedAt.property] === undefined) {
799+
this[updatedAt.property] = date
800+
}
801+
802+
/**
803+
* Forcing the deleted at column to be null to restore the model.
804+
*/
805+
if (deletedAt) {
806+
this[deletedAt.property] = null
807+
}
808+
809+
const data = this.dirty()
810+
811+
const where = { [primaryKey]: this[primaryKey] }
812+
const restored = await Model.restore(where, data)
766813

767814
Object.keys(restored).forEach(key => (this[key] = restored[key]))
768815

769-
return this
816+
return this.setOriginal()
770817
}
771818
}

src/models/builders/ModelQueryBuilder.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,21 +447,29 @@ export class ModelQueryBuilder<
447447
/**
448448
* Restore one or multiple soft deleted models.
449449
*/
450-
public async restore() {
450+
public async restore(data?: Partial<M>) {
451451
this.setInternalQueries({ addSoftDelete: false })
452452

453453
if (!this.DELETED_AT_PROP) {
454454
return
455455
}
456456

457+
const date = new Date()
457458
const updatedAt = this.schema.getUpdatedAtColumn()
458-
const data = { [this.DELETED_AT_PROP]: null } as any
459+
const attributes = this.isToSetAttributes ? this.Model.attributes() : {}
460+
461+
const parsed = this.schema.propertiesToColumnNames(
462+
{ ...data, [this.DELETED_AT_PROP]: null } as any,
463+
{
464+
attributes
465+
}
466+
)
459467

460-
if (updatedAt) {
461-
data[updatedAt.name] = new Date()
468+
if (updatedAt && parsed[updatedAt.name] === undefined) {
469+
parsed[updatedAt.name] = date
462470
}
463471

464-
const updated = await super.update(data)
472+
const updated = await super.update(parsed)
465473

466474
if (Is.Array(updated)) {
467475
return this.generator.generateMany(updated)

tests/unit/models/BaseModelTest.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,217 @@ export default class BaseModelTest {
831831
assert.calledTimes(Database.driver.update, 2)
832832
}
833833

834+
@Test()
835+
public async shouldBeAbleToRestoreAModelAndSaveOtherChangesSimultaneously({ assert }: Context) {
836+
Mock.when(Database.driver, 'find').resolve(undefined)
837+
Mock.when(Database.driver, 'update').resolve({ id: '1', name: 'txsoura', deletedAt: null })
838+
Mock.when(Database.driver, 'where').return(undefined)
839+
840+
const user = new User()
841+
842+
user.id = '1'
843+
user.name = 'lenon'
844+
user.email = 'lenon@athenna.io'
845+
user.metadata1 = 'random-1'
846+
user.metadata2 = 'random-2'
847+
user.createdAt = new Date()
848+
user.updatedAt = new Date()
849+
user.deletedAt = new Date()
850+
851+
user.setOriginal()
852+
853+
user.name = 'txsoura'
854+
855+
await user.restore()
856+
857+
assert.isNull(user.deletedAt)
858+
assert.deepEqual(user.name, 'txsoura')
859+
assert.calledWith(Database.driver.update, Mock.match({ name: 'txsoura', deleted_at: null }))
860+
}
861+
862+
@Test()
863+
public async shouldBeAbleToRestoreAModelWithMultipleChanges({ assert }: Context) {
864+
Mock.when(Database.driver, 'find').resolve(undefined)
865+
Mock.when(Database.driver, 'update').resolve({
866+
id: '1',
867+
name: 'txsoura',
868+
email: 'txsoura@athenna.io',
869+
deletedAt: null
870+
})
871+
Mock.when(Database.driver, 'where').return(undefined)
872+
873+
const user = new User()
874+
875+
user.id = '1'
876+
user.name = 'lenon'
877+
user.email = 'lenon@athenna.io'
878+
user.metadata1 = 'random-1'
879+
user.metadata2 = 'random-2'
880+
user.createdAt = new Date()
881+
user.updatedAt = new Date()
882+
user.deletedAt = new Date()
883+
884+
user.setOriginal()
885+
886+
user.name = 'txsoura'
887+
user.email = 'txsoura@athenna.io'
888+
889+
await user.restore()
890+
891+
assert.isNull(user.deletedAt)
892+
assert.deepEqual(user.name, 'txsoura')
893+
assert.deepEqual(user.email, 'txsoura@athenna.io')
894+
assert.calledWith(
895+
Database.driver.update,
896+
Mock.match({
897+
name: 'txsoura',
898+
email: 'txsoura@athenna.io',
899+
deleted_at: null
900+
})
901+
)
902+
}
903+
904+
@Test()
905+
public async shouldApplyAttributesWhenRestoringModel({ assert }: Context) {
906+
Mock.when(Database.driver, 'find').resolve(undefined)
907+
Mock.when(Database.driver, 'update').resolve({ id: '1', name: 'lenon', deletedAt: null })
908+
Mock.when(Database.driver, 'where').return(undefined)
909+
910+
const user = new User()
911+
912+
user.id = '1'
913+
user.name = 'lenon'
914+
user.createdAt = new Date()
915+
user.updatedAt = new Date()
916+
user.deletedAt = new Date()
917+
918+
user.setOriginal()
919+
920+
await user.restore()
921+
922+
assert.calledWith(
923+
Database.driver.update,
924+
Mock.match({
925+
deleted_at: null,
926+
metadata1: 'random-1',
927+
metadata2: 'random-2'
928+
})
929+
)
930+
}
931+
932+
@Test()
933+
public async shouldUpdateTimestampsWhenRestoringModel({ assert }: Context) {
934+
Mock.when(Database.driver, 'find').resolve(undefined)
935+
Mock.when(Database.driver, 'update').resolve({ id: '1', deletedAt: null })
936+
Mock.when(Database.driver, 'where').return(undefined)
937+
938+
const user = new User()
939+
940+
user.id = '1'
941+
user.name = 'lenon'
942+
user.createdAt = new Date()
943+
user.updatedAt = new Date()
944+
user.deletedAt = new Date()
945+
946+
user.setOriginal()
947+
948+
await user.restore()
949+
950+
assert.calledWith(Database.driver.update, Mock.match({ deleted_at: null }))
951+
assert.calledOnce(Database.driver.update)
952+
}
953+
954+
@Test()
955+
public async shouldBeAbleToRestoreModelWithoutPriorChanges({ assert }: Context) {
956+
Mock.when(Database.driver, 'find').resolve(undefined)
957+
Mock.when(Database.driver, 'update').resolve({ id: '1', deletedAt: null })
958+
Mock.when(Database.driver, 'where').return(undefined)
959+
960+
const user = new User()
961+
962+
user.id = '1'
963+
user.name = 'lenon'
964+
user.email = 'lenon@athenna.io'
965+
user.metadata1 = 'random-1'
966+
user.metadata2 = 'random-2'
967+
user.createdAt = new Date()
968+
user.updatedAt = new Date()
969+
user.deletedAt = new Date()
970+
971+
user.setOriginal()
972+
973+
await user.restore()
974+
975+
assert.isNull(user.deletedAt)
976+
assert.calledWith(Database.driver.update, Mock.match({ deleted_at: null }))
977+
}
978+
979+
@Test()
980+
public async shouldBeAbleToRestoreModelUsingStaticMethod({ assert }: Context) {
981+
Mock.when(Database.driver, 'find').resolve(undefined)
982+
Mock.when(Database.driver, 'update').resolve({ id: '1', name: 'txsoura', deletedAt: null })
983+
Mock.when(Database.driver, 'where').return(undefined)
984+
985+
await User.restore({ id: '1' }, { name: 'txsoura' })
986+
987+
assert.calledWith(
988+
Database.driver.update,
989+
Mock.match({
990+
name: 'txsoura',
991+
deleted_at: null
992+
})
993+
)
994+
}
995+
996+
@Test()
997+
public async shouldBeAbleToRestoreMultipleModelsUsingStaticMethod({ assert }: Context) {
998+
Mock.when(Database.driver, 'find').resolve(undefined)
999+
Mock.when(Database.driver, 'update').resolve([
1000+
{ id: '1', name: 'txsoura', deletedAt: null },
1001+
{ id: '2', name: 'txsoura', deletedAt: null }
1002+
])
1003+
Mock.when(Database.driver, 'where').return(undefined)
1004+
1005+
const users = await User.restore({ name: 'lenon' }, { name: 'txsoura' })
1006+
1007+
assert.isArray(users)
1008+
assert.lengthOf(users as User[], 2)
1009+
assert.calledWith(
1010+
Database.driver.update,
1011+
Mock.match({
1012+
name: 'txsoura',
1013+
deleted_at: null
1014+
})
1015+
)
1016+
}
1017+
1018+
@Test()
1019+
public async shouldSetOriginalAfterRestoringModel({ assert }: Context) {
1020+
Mock.when(Database.driver, 'find').resolve(undefined)
1021+
Mock.when(Database.driver, 'update').resolve({ id: '1', name: 'txsoura', deletedAt: null })
1022+
Mock.when(Database.driver, 'where').return(undefined)
1023+
1024+
const user = new User()
1025+
1026+
user.id = '1'
1027+
user.name = 'lenon'
1028+
user.metadata1 = 'random-1'
1029+
user.metadata2 = 'random-2'
1030+
user.createdAt = new Date()
1031+
user.updatedAt = new Date()
1032+
user.deletedAt = new Date()
1033+
1034+
user.setOriginal()
1035+
1036+
user.name = 'txsoura'
1037+
1038+
await user.restore()
1039+
1040+
assert.isFalse(user.isDirty())
1041+
assert.deepEqual(user.name, 'txsoura')
1042+
assert.isNull(user.deletedAt)
1043+
}
1044+
8341045
@Test()
8351046
public async shouldBeAbleToUseFakerProperty({ assert }: Context) {
8361047
assert.isTrue(BaseModel.faker.internet.email().includes('@'))

0 commit comments

Comments
 (0)