@declaro/data 2.0.0-beta.125 → 2.0.0-beta.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.js +11 -11
- package/dist/browser/index.js.map +6 -6
- package/dist/node/index.cjs +189 -92
- package/dist/node/index.cjs.map +6 -6
- package/dist/node/index.js +181 -84
- package/dist/node/index.js.map +6 -6
- package/dist/ts/application/model-controller.d.ts +15 -5
- package/dist/ts/application/model-controller.d.ts.map +1 -1
- package/dist/ts/application/read-only-model-controller.d.ts +5 -1
- package/dist/ts/application/read-only-model-controller.d.ts.map +1 -1
- package/dist/ts/domain/services/model-service.d.ts +8 -0
- package/dist/ts/domain/services/model-service.d.ts.map +1 -1
- package/dist/ts/domain/services/read-only-model-service.d.ts +8 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -1
- package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
- package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
- package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
- package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
- package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
- package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
- package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/application/model-controller.ts +110 -59
- package/src/application/read-only-model-controller.ts +43 -25
- package/src/domain/services/model-service.test.ts +280 -0
- package/src/domain/services/model-service.ts +98 -66
- package/src/domain/services/read-only-model-service.test.ts +230 -0
- package/src/domain/services/read-only-model-service.ts +65 -40
- package/src/shared/utils/schema-inheritance.test.ts +295 -0
- package/src/shared/utils/test/animal-schema.ts +46 -0
- package/src/shared/utils/test/animal-trait-schema.ts +45 -0
- package/src/shared/utils/test/elephant-schema.ts +58 -0
- package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts +0 -1
- package/dist/ts/test/mock/repositories/mock-memory-repository.custom-lookup.test.d.ts.map +0 -1
- package/src/test/mock/repositories/mock-memory-repository.custom-lookup.test.ts +0 -0
|
@@ -1,52 +1,92 @@
|
|
|
1
1
|
import type { AuthValidator } from '@declaro/auth'
|
|
2
2
|
import { PermissionValidator, type AnyModelSchema } from '@declaro/core'
|
|
3
3
|
import type { ModelService, ICreateOptions, IUpdateOptions } from '../domain/services/model-service'
|
|
4
|
+
import type { ILoadOptions } from '../domain/services/read-only-model-service'
|
|
4
5
|
import type { InferDetail, InferFilters, InferInput, InferLookup, InferSummary } from '../shared/utils/schema-inference'
|
|
5
6
|
import { ReadOnlyModelController } from './read-only-model-controller'
|
|
6
7
|
|
|
7
8
|
export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyModelController<TSchema> {
|
|
8
|
-
constructor(
|
|
9
|
+
constructor(
|
|
10
|
+
protected readonly service: ModelService<TSchema>,
|
|
11
|
+
protected readonly authValidator: AuthValidator,
|
|
12
|
+
) {
|
|
9
13
|
super(service, authValidator)
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
async
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
async createPermissions(input: InferInput<TSchema>, options?: ICreateOptions): Promise<PermissionValidator> {
|
|
17
|
+
return PermissionValidator.create().someOf([
|
|
18
|
+
this.service.getDescriptor('create', '*').toString(),
|
|
19
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async create(input: InferInput<TSchema>, options?: ICreateOptions): Promise<InferDetail<TSchema>> {
|
|
24
|
+
const permissions = await this.createPermissions(input, options)
|
|
25
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
26
|
+
return this.service.create(input, options)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async updatePermissions(
|
|
30
|
+
lookup: InferLookup<TSchema>,
|
|
31
|
+
input: InferInput<TSchema>,
|
|
32
|
+
options?: IUpdateOptions,
|
|
33
|
+
): Promise<PermissionValidator> {
|
|
34
|
+
return PermissionValidator.create().someOf([
|
|
35
|
+
this.service.getDescriptor('update', '*').toString(),
|
|
36
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async update(
|
|
41
|
+
lookup: InferLookup<TSchema>,
|
|
42
|
+
input: InferInput<TSchema>,
|
|
43
|
+
options?: IUpdateOptions,
|
|
44
|
+
): Promise<InferDetail<TSchema>> {
|
|
45
|
+
const permissions = await this.updatePermissions(lookup, input, options)
|
|
46
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
47
|
+
return this.service.update(lookup, input, options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async removePermissions(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<PermissionValidator> {
|
|
51
|
+
return PermissionValidator.create().someOf([
|
|
52
|
+
this.service.getDescriptor('remove', '*').toString(),
|
|
53
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
54
|
+
])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async remove(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferSummary<TSchema>> {
|
|
58
|
+
const permissions = await this.removePermissions(lookup, options)
|
|
59
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
60
|
+
return this.service.remove(lookup, options)
|
|
20
61
|
}
|
|
21
62
|
|
|
22
|
-
async
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
]),
|
|
28
|
-
)
|
|
29
|
-
return this.service.update(lookup, input)
|
|
63
|
+
async restorePermissions(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<PermissionValidator> {
|
|
64
|
+
return PermissionValidator.create().someOf([
|
|
65
|
+
this.service.getDescriptor('restore', '*').toString(),
|
|
66
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
67
|
+
])
|
|
30
68
|
}
|
|
31
69
|
|
|
32
|
-
async
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.service.getDescriptor('write', '*').toString(),
|
|
37
|
-
]),
|
|
38
|
-
)
|
|
39
|
-
return this.service.remove(lookup)
|
|
70
|
+
async restore(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferSummary<TSchema>> {
|
|
71
|
+
const permissions = await this.restorePermissions(lookup, options)
|
|
72
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
73
|
+
return this.service.restore(lookup, options)
|
|
40
74
|
}
|
|
41
75
|
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
async upsertPermissions(
|
|
77
|
+
input: InferInput<TSchema>,
|
|
78
|
+
options?: ICreateOptions | IUpdateOptions,
|
|
79
|
+
): Promise<PermissionValidator> {
|
|
80
|
+
// Create nested validator for (create AND update) permissions
|
|
81
|
+
const createAndUpdateValidator = PermissionValidator.create().allOf([
|
|
82
|
+
this.service.getDescriptor('create', '*').toString(),
|
|
83
|
+
this.service.getDescriptor('update', '*').toString(),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
return PermissionValidator.create().someOf([
|
|
87
|
+
createAndUpdateValidator,
|
|
88
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
89
|
+
])
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
/**
|
|
@@ -56,16 +96,25 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
|
|
|
56
96
|
* @returns The upserted record.
|
|
57
97
|
*/
|
|
58
98
|
async upsert(input: InferInput<TSchema>, options?: ICreateOptions | IUpdateOptions): Promise<InferDetail<TSchema>> {
|
|
99
|
+
const permissions = await this.upsertPermissions(input, options)
|
|
100
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
101
|
+
return this.service.upsert(input, options)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async bulkUpsertPermissions(
|
|
105
|
+
inputs: InferInput<TSchema>[],
|
|
106
|
+
options?: ICreateOptions | IUpdateOptions,
|
|
107
|
+
): Promise<PermissionValidator> {
|
|
59
108
|
// Create nested validator for (create AND update) permissions
|
|
60
109
|
const createAndUpdateValidator = PermissionValidator.create().allOf([
|
|
61
110
|
this.service.getDescriptor('create', '*').toString(),
|
|
62
111
|
this.service.getDescriptor('update', '*').toString(),
|
|
63
112
|
])
|
|
64
113
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
return PermissionValidator.create().someOf([
|
|
115
|
+
createAndUpdateValidator,
|
|
116
|
+
this.service.getDescriptor('write', '*').toString(),
|
|
117
|
+
])
|
|
69
118
|
}
|
|
70
119
|
|
|
71
120
|
/**
|
|
@@ -78,18 +127,19 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
|
|
|
78
127
|
inputs: InferInput<TSchema>[],
|
|
79
128
|
options?: ICreateOptions | IUpdateOptions,
|
|
80
129
|
): Promise<InferDetail<TSchema>[]> {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.service.getDescriptor('create', '*').toString(),
|
|
84
|
-
this.service.getDescriptor('update', '*').toString(),
|
|
85
|
-
])
|
|
86
|
-
|
|
87
|
-
this.authValidator.validatePermissions((v) =>
|
|
88
|
-
v.someOf([createAndUpdateValidator, this.service.getDescriptor('write', '*').toString()]),
|
|
89
|
-
)
|
|
130
|
+
const permissions = await this.bulkUpsertPermissions(inputs, options)
|
|
131
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
90
132
|
return this.service.bulkUpsert(inputs, options)
|
|
91
133
|
}
|
|
92
134
|
|
|
135
|
+
async permanentlyDeleteFromTrashPermissions(lookup: InferLookup<TSchema>): Promise<PermissionValidator> {
|
|
136
|
+
return PermissionValidator.create().someOf([
|
|
137
|
+
this.service.getDescriptor('permanently-delete-from-trash', '*').toString(),
|
|
138
|
+
this.service.getDescriptor('permanently-delete', '*').toString(),
|
|
139
|
+
this.service.getDescriptor('empty-trash', '*').toString(),
|
|
140
|
+
])
|
|
141
|
+
}
|
|
142
|
+
|
|
93
143
|
/**
|
|
94
144
|
* Permanently deletes a specific entity from the trash.
|
|
95
145
|
* Requires 'permanently-delete-from-trash', 'permanently-delete', or 'empty-trash' permission.
|
|
@@ -97,16 +147,15 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
|
|
|
97
147
|
* @returns The permanently deleted entity summary
|
|
98
148
|
*/
|
|
99
149
|
async permanentlyDeleteFromTrash(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
this.service.getDescriptor('permanently-delete-from-trash', '*').toString(),
|
|
103
|
-
this.service.getDescriptor('permanently-delete', '*').toString(),
|
|
104
|
-
this.service.getDescriptor('empty-trash', '*').toString(),
|
|
105
|
-
]),
|
|
106
|
-
)
|
|
150
|
+
const permissions = await this.permanentlyDeleteFromTrashPermissions(lookup)
|
|
151
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
107
152
|
return this.service.permanentlyDeleteFromTrash(lookup)
|
|
108
153
|
}
|
|
109
154
|
|
|
155
|
+
async permanentlyDeletePermissions(lookup: InferLookup<TSchema>): Promise<PermissionValidator> {
|
|
156
|
+
return PermissionValidator.create().someOf([this.service.getDescriptor('permanently-delete', '*').toString()])
|
|
157
|
+
}
|
|
158
|
+
|
|
110
159
|
/**
|
|
111
160
|
* Permanently deletes an entity without moving it to trash first.
|
|
112
161
|
* Requires 'permanently-delete' permission.
|
|
@@ -114,12 +163,15 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
|
|
|
114
163
|
* @returns The permanently deleted entity summary
|
|
115
164
|
*/
|
|
116
165
|
async permanentlyDelete(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
)
|
|
166
|
+
const permissions = await this.permanentlyDeletePermissions(lookup)
|
|
167
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
120
168
|
return this.service.permanentlyDelete(lookup)
|
|
121
169
|
}
|
|
122
170
|
|
|
171
|
+
async emptyTrashPermissions(filters?: InferFilters<TSchema>): Promise<PermissionValidator> {
|
|
172
|
+
return PermissionValidator.create().someOf([this.service.getDescriptor('empty-trash', '*').toString()])
|
|
173
|
+
}
|
|
174
|
+
|
|
123
175
|
/**
|
|
124
176
|
* Empties the trash by permanently deleting entities that have been marked as removed.
|
|
125
177
|
* Requires 'empty-trash' permission.
|
|
@@ -127,9 +179,8 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
|
|
|
127
179
|
* @returns The count of entities permanently deleted
|
|
128
180
|
*/
|
|
129
181
|
async emptyTrash(filters?: InferFilters<TSchema>): Promise<number> {
|
|
130
|
-
this.
|
|
131
|
-
|
|
132
|
-
)
|
|
182
|
+
const permissions = await this.emptyTrashPermissions(filters)
|
|
183
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
133
184
|
return this.service.emptyTrash(filters)
|
|
134
185
|
}
|
|
135
186
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthValidator } from '@declaro/auth'
|
|
2
2
|
import {} from '@declaro/auth'
|
|
3
|
-
import type
|
|
3
|
+
import { PermissionValidator, type AnyModelSchema } from '@declaro/core'
|
|
4
4
|
import type { ILoadOptions, ISearchOptions, ReadOnlyModelService } from '../domain/services/read-only-model-service'
|
|
5
5
|
import type { InferDetail, InferFilters, InferLookup, InferSearchResults } from '../shared/utils/schema-inference'
|
|
6
6
|
|
|
@@ -10,39 +10,61 @@ export class ReadOnlyModelController<TSchema extends AnyModelSchema> {
|
|
|
10
10
|
protected readonly authValidator: AuthValidator,
|
|
11
11
|
) {}
|
|
12
12
|
|
|
13
|
+
async loadPermissions(lookup: InferLookup<TSchema>): Promise<PermissionValidator> {
|
|
14
|
+
return PermissionValidator.create().someOf([
|
|
15
|
+
this.service.getDescriptor('load', '*').toString(),
|
|
16
|
+
this.service.getDescriptor('read', '*').toString(),
|
|
17
|
+
])
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
async load(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferDetail<TSchema>> {
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
this.service.getDescriptor('load', '*').toString(),
|
|
17
|
-
this.service.getDescriptor('read', '*').toString(),
|
|
18
|
-
]),
|
|
19
|
-
)
|
|
21
|
+
const permissions = await this.loadPermissions(lookup)
|
|
22
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
20
23
|
return this.service.load(lookup, options)
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
async loadManyPermissions(lookups: InferLookup<TSchema>[]): Promise<PermissionValidator> {
|
|
27
|
+
return PermissionValidator.create().someOf([
|
|
28
|
+
this.service.getDescriptor('loadMany', '*').toString(),
|
|
29
|
+
this.service.getDescriptor('read', '*').toString(),
|
|
30
|
+
])
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
async loadMany(lookups: InferLookup<TSchema>[], options?: ILoadOptions): Promise<InferDetail<TSchema>[]> {
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
this.service.getDescriptor('loadMany', '*').toString(),
|
|
27
|
-
this.service.getDescriptor('read', '*').toString(),
|
|
28
|
-
]),
|
|
29
|
-
)
|
|
34
|
+
const permissions = await this.loadManyPermissions(lookups)
|
|
35
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
30
36
|
return this.service.loadMany(lookups, options)
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
async searchPermissions(
|
|
40
|
+
input: InferFilters<TSchema>,
|
|
41
|
+
options?: ISearchOptions<TSchema>,
|
|
42
|
+
): Promise<PermissionValidator> {
|
|
43
|
+
return PermissionValidator.create().someOf([
|
|
44
|
+
this.service.getDescriptor('search', '*').toString(),
|
|
45
|
+
this.service.getDescriptor('read', '*').toString(),
|
|
46
|
+
])
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
async search(
|
|
34
50
|
input: InferFilters<TSchema>,
|
|
35
51
|
options?: ISearchOptions<TSchema>,
|
|
36
52
|
): Promise<InferSearchResults<TSchema>> {
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
this.service.getDescriptor('search', '*').toString(),
|
|
40
|
-
this.service.getDescriptor('read', '*').toString(),
|
|
41
|
-
]),
|
|
42
|
-
)
|
|
53
|
+
const permissions = await this.searchPermissions(input, options)
|
|
54
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
43
55
|
return this.service.search(input, options)
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
async countPermissions(
|
|
59
|
+
input: InferFilters<TSchema>,
|
|
60
|
+
options?: ISearchOptions<TSchema>,
|
|
61
|
+
): Promise<PermissionValidator> {
|
|
62
|
+
return PermissionValidator.create().someOf([
|
|
63
|
+
this.service.getDescriptor('count', '*').toString(),
|
|
64
|
+
this.service.getDescriptor('read', '*').toString(),
|
|
65
|
+
])
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
/**
|
|
47
69
|
* Count the number of records matching the given filters.
|
|
48
70
|
* @param input The filters to apply to the count operation.
|
|
@@ -50,12 +72,8 @@ export class ReadOnlyModelController<TSchema extends AnyModelSchema> {
|
|
|
50
72
|
* @returns The count of matching records.
|
|
51
73
|
*/
|
|
52
74
|
async count(input: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): Promise<number> {
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
this.service.getDescriptor('count', '*').toString(),
|
|
56
|
-
this.service.getDescriptor('read', '*').toString(),
|
|
57
|
-
]),
|
|
58
|
-
)
|
|
75
|
+
const permissions = await this.countPermissions(input, options)
|
|
76
|
+
this.authValidator.validatePermissions((v) => v.extend(permissions))
|
|
59
77
|
return this.service.count(input, options)
|
|
60
78
|
}
|
|
61
79
|
}
|
|
@@ -934,4 +934,284 @@ describe('ModelService', () => {
|
|
|
934
934
|
})
|
|
935
935
|
})
|
|
936
936
|
})
|
|
937
|
+
|
|
938
|
+
describe('doNotDispatchEvents option', () => {
|
|
939
|
+
const beforeLoadSpy = mock(() => {})
|
|
940
|
+
const afterLoadSpy = mock(() => {})
|
|
941
|
+
const beforeLoadManySpy = mock(() => {})
|
|
942
|
+
const afterLoadManySpy = mock(() => {})
|
|
943
|
+
|
|
944
|
+
beforeEach(() => {
|
|
945
|
+
repository = new MockMemoryRepository({ schema: mockSchema })
|
|
946
|
+
emitter = new EventManager()
|
|
947
|
+
|
|
948
|
+
beforeLoadSpy.mockClear()
|
|
949
|
+
afterLoadSpy.mockClear()
|
|
950
|
+
beforeLoadManySpy.mockClear()
|
|
951
|
+
afterLoadManySpy.mockClear()
|
|
952
|
+
beforeCreateSpy.mockClear()
|
|
953
|
+
afterCreateSpy.mockClear()
|
|
954
|
+
beforeUpdateSpy.mockClear()
|
|
955
|
+
afterUpdateSpy.mockClear()
|
|
956
|
+
|
|
957
|
+
emitter.on('books::book.beforeLoad', beforeLoadSpy)
|
|
958
|
+
emitter.on('books::book.afterLoad', afterLoadSpy)
|
|
959
|
+
emitter.on('books::book.beforeLoadMany', beforeLoadManySpy)
|
|
960
|
+
emitter.on('books::book.afterLoadMany', afterLoadManySpy)
|
|
961
|
+
emitter.on('books::book.beforeCreate', beforeCreateSpy)
|
|
962
|
+
emitter.on('books::book.afterCreate', afterCreateSpy)
|
|
963
|
+
emitter.on('books::book.beforeUpdate', beforeUpdateSpy)
|
|
964
|
+
emitter.on('books::book.afterUpdate', afterUpdateSpy)
|
|
965
|
+
|
|
966
|
+
service = new ModelService({ repository, emitter, schema: mockSchema, namespace })
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
describe('upsert', () => {
|
|
970
|
+
it('should not dispatch any events when doNotDispatchEvents is true', async () => {
|
|
971
|
+
const input = {
|
|
972
|
+
id: 1,
|
|
973
|
+
title: 'Existing Book',
|
|
974
|
+
author: 'Author',
|
|
975
|
+
publishedDate: new Date(),
|
|
976
|
+
}
|
|
977
|
+
await repository.create(input)
|
|
978
|
+
|
|
979
|
+
const updatedInput = {
|
|
980
|
+
id: 1,
|
|
981
|
+
title: 'Updated Book',
|
|
982
|
+
author: 'Updated Author',
|
|
983
|
+
publishedDate: new Date(),
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
await service.upsert(updatedInput, { doNotDispatchEvents: true })
|
|
987
|
+
|
|
988
|
+
// Load events should not be dispatched due to propagation
|
|
989
|
+
expect(beforeLoadSpy).not.toHaveBeenCalled()
|
|
990
|
+
expect(afterLoadSpy).not.toHaveBeenCalled()
|
|
991
|
+
|
|
992
|
+
// Update events should not be dispatched either
|
|
993
|
+
expect(beforeUpdateSpy).not.toHaveBeenCalled()
|
|
994
|
+
expect(afterUpdateSpy).not.toHaveBeenCalled()
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
it('should not dispatch load events even when doNotDispatchEvents is false', async () => {
|
|
998
|
+
const input = {
|
|
999
|
+
id: 2,
|
|
1000
|
+
title: 'Existing Book',
|
|
1001
|
+
author: 'Author',
|
|
1002
|
+
publishedDate: new Date(),
|
|
1003
|
+
}
|
|
1004
|
+
await repository.create(input)
|
|
1005
|
+
|
|
1006
|
+
const updatedInput = {
|
|
1007
|
+
id: 2,
|
|
1008
|
+
title: 'Updated Book',
|
|
1009
|
+
author: 'Updated Author',
|
|
1010
|
+
publishedDate: new Date(),
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
await service.upsert(updatedInput, { doNotDispatchEvents: false })
|
|
1014
|
+
|
|
1015
|
+
// Load events should NOT be dispatched (forced internally)
|
|
1016
|
+
expect(beforeLoadSpy).not.toHaveBeenCalled()
|
|
1017
|
+
expect(afterLoadSpy).not.toHaveBeenCalled()
|
|
1018
|
+
|
|
1019
|
+
// Update events should be dispatched
|
|
1020
|
+
expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
|
|
1021
|
+
expect(afterUpdateSpy).toHaveBeenCalledTimes(1)
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it('should not dispatch load events even when doNotDispatchEvents is not specified', async () => {
|
|
1025
|
+
const input = {
|
|
1026
|
+
id: 3,
|
|
1027
|
+
title: 'Existing Book',
|
|
1028
|
+
author: 'Author',
|
|
1029
|
+
publishedDate: new Date(),
|
|
1030
|
+
}
|
|
1031
|
+
await repository.create(input)
|
|
1032
|
+
|
|
1033
|
+
const updatedInput = {
|
|
1034
|
+
id: 3,
|
|
1035
|
+
title: 'Updated Book',
|
|
1036
|
+
author: 'Updated Author',
|
|
1037
|
+
publishedDate: new Date(),
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
await service.upsert(updatedInput)
|
|
1041
|
+
|
|
1042
|
+
// Load events should NOT be dispatched (forced internally)
|
|
1043
|
+
expect(beforeLoadSpy).not.toHaveBeenCalled()
|
|
1044
|
+
expect(afterLoadSpy).not.toHaveBeenCalled()
|
|
1045
|
+
|
|
1046
|
+
// Update events should be dispatched
|
|
1047
|
+
expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
|
|
1048
|
+
expect(afterUpdateSpy).toHaveBeenCalledTimes(1)
|
|
1049
|
+
})
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
describe('bulkUpsert', () => {
|
|
1053
|
+
it('should not dispatch any events when doNotDispatchEvents is true', async () => {
|
|
1054
|
+
const input1 = {
|
|
1055
|
+
id: 1,
|
|
1056
|
+
title: 'Existing Book 1',
|
|
1057
|
+
author: 'Author 1',
|
|
1058
|
+
publishedDate: new Date(),
|
|
1059
|
+
}
|
|
1060
|
+
const input2 = {
|
|
1061
|
+
id: 2,
|
|
1062
|
+
title: 'Existing Book 2',
|
|
1063
|
+
author: 'Author 2',
|
|
1064
|
+
publishedDate: new Date(),
|
|
1065
|
+
}
|
|
1066
|
+
await repository.create(input1)
|
|
1067
|
+
await repository.create(input2)
|
|
1068
|
+
|
|
1069
|
+
const updatedInputs = [
|
|
1070
|
+
{
|
|
1071
|
+
id: 1,
|
|
1072
|
+
title: 'Updated Book 1',
|
|
1073
|
+
author: 'Updated Author 1',
|
|
1074
|
+
publishedDate: new Date(),
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
id: 2,
|
|
1078
|
+
title: 'Updated Book 2',
|
|
1079
|
+
author: 'Updated Author 2',
|
|
1080
|
+
publishedDate: new Date(),
|
|
1081
|
+
},
|
|
1082
|
+
]
|
|
1083
|
+
|
|
1084
|
+
await service.bulkUpsert(updatedInputs, { doNotDispatchEvents: true })
|
|
1085
|
+
|
|
1086
|
+
// LoadMany events should not be dispatched due to propagation
|
|
1087
|
+
expect(beforeLoadManySpy).not.toHaveBeenCalled()
|
|
1088
|
+
expect(afterLoadManySpy).not.toHaveBeenCalled()
|
|
1089
|
+
|
|
1090
|
+
// Update events should not be dispatched either
|
|
1091
|
+
expect(beforeUpdateSpy).not.toHaveBeenCalled()
|
|
1092
|
+
expect(afterUpdateSpy).not.toHaveBeenCalled()
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('should not dispatch loadMany events even when doNotDispatchEvents is false', async () => {
|
|
1096
|
+
const input1 = {
|
|
1097
|
+
id: 3,
|
|
1098
|
+
title: 'Existing Book 3',
|
|
1099
|
+
author: 'Author 3',
|
|
1100
|
+
publishedDate: new Date(),
|
|
1101
|
+
}
|
|
1102
|
+
const input2 = {
|
|
1103
|
+
id: 4,
|
|
1104
|
+
title: 'Existing Book 4',
|
|
1105
|
+
author: 'Author 4',
|
|
1106
|
+
publishedDate: new Date(),
|
|
1107
|
+
}
|
|
1108
|
+
await repository.create(input1)
|
|
1109
|
+
await repository.create(input2)
|
|
1110
|
+
|
|
1111
|
+
const updatedInputs = [
|
|
1112
|
+
{
|
|
1113
|
+
id: 3,
|
|
1114
|
+
title: 'Updated Book 3',
|
|
1115
|
+
author: 'Updated Author 3',
|
|
1116
|
+
publishedDate: new Date(),
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
id: 4,
|
|
1120
|
+
title: 'Updated Book 4',
|
|
1121
|
+
author: 'Updated Author 4',
|
|
1122
|
+
publishedDate: new Date(),
|
|
1123
|
+
},
|
|
1124
|
+
]
|
|
1125
|
+
|
|
1126
|
+
await service.bulkUpsert(updatedInputs, { doNotDispatchEvents: false })
|
|
1127
|
+
|
|
1128
|
+
// LoadMany events should NOT be dispatched (forced internally)
|
|
1129
|
+
expect(beforeLoadManySpy).not.toHaveBeenCalled()
|
|
1130
|
+
expect(afterLoadManySpy).not.toHaveBeenCalled()
|
|
1131
|
+
|
|
1132
|
+
// Update events should be dispatched (2 updates)
|
|
1133
|
+
expect(beforeUpdateSpy).toHaveBeenCalledTimes(2)
|
|
1134
|
+
expect(afterUpdateSpy).toHaveBeenCalledTimes(2)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
it('should not dispatch loadMany events even when doNotDispatchEvents is not specified', async () => {
|
|
1138
|
+
const input1 = {
|
|
1139
|
+
id: 5,
|
|
1140
|
+
title: 'Existing Book 5',
|
|
1141
|
+
author: 'Author 5',
|
|
1142
|
+
publishedDate: new Date(),
|
|
1143
|
+
}
|
|
1144
|
+
const input2 = {
|
|
1145
|
+
id: 6,
|
|
1146
|
+
title: 'Existing Book 6',
|
|
1147
|
+
author: 'Author 6',
|
|
1148
|
+
publishedDate: new Date(),
|
|
1149
|
+
}
|
|
1150
|
+
await repository.create(input1)
|
|
1151
|
+
await repository.create(input2)
|
|
1152
|
+
|
|
1153
|
+
const updatedInputs = [
|
|
1154
|
+
{
|
|
1155
|
+
id: 5,
|
|
1156
|
+
title: 'Updated Book 5',
|
|
1157
|
+
author: 'Updated Author 5',
|
|
1158
|
+
publishedDate: new Date(),
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
id: 6,
|
|
1162
|
+
title: 'Updated Book 6',
|
|
1163
|
+
author: 'Updated Author 6',
|
|
1164
|
+
publishedDate: new Date(),
|
|
1165
|
+
},
|
|
1166
|
+
]
|
|
1167
|
+
|
|
1168
|
+
await service.bulkUpsert(updatedInputs)
|
|
1169
|
+
|
|
1170
|
+
// LoadMany events should NOT be dispatched (forced internally)
|
|
1171
|
+
expect(beforeLoadManySpy).not.toHaveBeenCalled()
|
|
1172
|
+
expect(afterLoadManySpy).not.toHaveBeenCalled()
|
|
1173
|
+
|
|
1174
|
+
// Update events should be dispatched (2 updates)
|
|
1175
|
+
expect(beforeUpdateSpy).toHaveBeenCalledTimes(2)
|
|
1176
|
+
expect(afterUpdateSpy).toHaveBeenCalledTimes(2)
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
it('should not dispatch any events with doNotDispatchEvents for mixed create and update operations', async () => {
|
|
1180
|
+
const existingInput = {
|
|
1181
|
+
id: 7,
|
|
1182
|
+
title: 'Existing Book',
|
|
1183
|
+
author: 'Author',
|
|
1184
|
+
publishedDate: new Date(),
|
|
1185
|
+
}
|
|
1186
|
+
await repository.create(existingInput)
|
|
1187
|
+
|
|
1188
|
+
const inputs = [
|
|
1189
|
+
{
|
|
1190
|
+
id: 7,
|
|
1191
|
+
title: 'Updated Book',
|
|
1192
|
+
author: 'Updated Author',
|
|
1193
|
+
publishedDate: new Date(),
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
id: 8,
|
|
1197
|
+
title: 'New Book',
|
|
1198
|
+
author: 'New Author',
|
|
1199
|
+
publishedDate: new Date(),
|
|
1200
|
+
},
|
|
1201
|
+
]
|
|
1202
|
+
|
|
1203
|
+
await service.bulkUpsert(inputs, { doNotDispatchEvents: true })
|
|
1204
|
+
|
|
1205
|
+
// LoadMany events should not be dispatched
|
|
1206
|
+
expect(beforeLoadManySpy).not.toHaveBeenCalled()
|
|
1207
|
+
expect(afterLoadManySpy).not.toHaveBeenCalled()
|
|
1208
|
+
|
|
1209
|
+
// Neither create nor update events should be dispatched
|
|
1210
|
+
expect(beforeCreateSpy).not.toHaveBeenCalled()
|
|
1211
|
+
expect(afterCreateSpy).not.toHaveBeenCalled()
|
|
1212
|
+
expect(beforeUpdateSpy).not.toHaveBeenCalled()
|
|
1213
|
+
expect(afterUpdateSpy).not.toHaveBeenCalled()
|
|
1214
|
+
})
|
|
1215
|
+
})
|
|
1216
|
+
})
|
|
937
1217
|
})
|