@declaro/data 2.0.0-beta.14 → 2.0.0-beta.140
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/{LICENSE → LICENSE.md} +1 -1
- package/README.md +0 -0
- package/dist/browser/index.js +26 -0
- package/dist/browser/index.js.map +93 -0
- package/dist/node/index.cjs +13372 -0
- package/dist/node/index.cjs.map +93 -0
- package/dist/node/index.js +13351 -0
- package/dist/node/index.js.map +93 -0
- package/dist/ts/application/model-controller.d.ts +60 -0
- package/dist/ts/application/model-controller.d.ts.map +1 -0
- package/dist/ts/application/model-controller.test.d.ts +2 -0
- package/dist/ts/application/model-controller.test.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.d.ts +24 -0
- package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
- package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.d.ts +41 -0
- package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
- package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
- package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/event-types.d.ts +37 -0
- package/dist/ts/domain/events/event-types.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.d.ts +41 -0
- package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
- package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.d.ts +8 -0
- package/dist/ts/domain/events/query-event.d.ts.map +1 -0
- package/dist/ts/domain/events/query-event.test.d.ts +2 -0
- package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.d.ts +26 -0
- package/dist/ts/domain/events/request-event.d.ts.map +1 -0
- package/dist/ts/domain/events/request-event.test.d.ts +2 -0
- package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
- package/dist/ts/domain/interfaces/repository.d.ts +110 -0
- package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
- package/dist/ts/domain/models/pagination.d.ts +28 -0
- package/dist/ts/domain/models/pagination.d.ts.map +1 -0
- package/dist/ts/domain/services/base-model-service.d.ts +23 -0
- package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service-args.d.ts +9 -0
- package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.d.ts +99 -0
- package/dist/ts/domain/services/model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
- package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
- package/dist/ts/domain/services/model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts +90 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
- package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
- package/dist/ts/index.d.ts +18 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
- package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
- package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
- 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/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
- package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
- package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
- package/package.json +46 -42
- package/src/application/model-controller.test.ts +694 -0
- package/src/application/model-controller.ts +186 -0
- package/src/application/read-only-model-controller.test.ts +335 -0
- package/src/application/read-only-model-controller.ts +79 -0
- package/src/domain/events/domain-event.test.ts +82 -0
- package/src/domain/events/domain-event.ts +69 -0
- package/src/domain/events/event-types.ts +37 -0
- package/src/domain/events/mutation-event.test.ts +390 -0
- package/src/domain/events/mutation-event.ts +53 -0
- package/src/domain/events/query-event.test.ts +228 -0
- package/src/domain/events/query-event.ts +14 -0
- package/src/domain/events/request-event.test.ts +38 -0
- package/src/domain/events/request-event.ts +47 -0
- package/src/domain/interfaces/repository.ts +136 -0
- package/src/domain/models/pagination.ts +28 -0
- package/src/domain/services/base-model-service.ts +54 -0
- package/src/domain/services/model-service-args.ts +9 -0
- package/src/domain/services/model-service.normalization.test.ts +704 -0
- package/src/domain/services/model-service.test.ts +1616 -0
- package/src/domain/services/model-service.ts +597 -0
- package/src/domain/services/read-only-model-service.test.ts +1130 -0
- package/src/domain/services/read-only-model-service.ts +211 -0
- package/src/index.ts +17 -4
- package/src/shared/utils/schema-inference.ts +26 -0
- package/src/shared/utils/schema-inheritance.test.ts +295 -0
- package/src/shared/utils/schema-inheritance.ts +28 -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/src/test/mock/models/mock-book-models.ts +78 -0
- package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
- package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
- package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
- package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
- package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
- package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
- package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
- package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
- package/dist/databaseConnection.d.ts +0 -24
- package/dist/datastoreAbstract.d.ts +0 -37
- package/dist/declaro-data.cjs +0 -1
- package/dist/declaro-data.mjs +0 -250
- package/dist/hydrateEntity.d.ts +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/serverConnection.d.ts +0 -15
- package/dist/trackedStatus.d.ts +0 -15
- package/src/databaseConnection.ts +0 -137
- package/src/datastoreAbstract.ts +0 -190
- package/src/hydrateEntity.ts +0 -36
- package/src/placeholder.test.ts +0 -7
- package/src/serverConnection.ts +0 -74
- package/src/trackedStatus.ts +0 -35
- package/tsconfig.json +0 -10
- package/vite.config.ts +0 -23
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import type { ActionDescriptor, AnyModelSchema, IActionDescriptor, IAnyModel } from '@declaro/core'
|
|
2
|
+
import type {
|
|
3
|
+
InferDetail,
|
|
4
|
+
InferFilters,
|
|
5
|
+
InferInput,
|
|
6
|
+
InferLookup,
|
|
7
|
+
InferSummary,
|
|
8
|
+
} from '../../shared/utils/schema-inference'
|
|
9
|
+
import { ModelMutationAction, ModelQueryEvent } from '../events/event-types'
|
|
10
|
+
import {
|
|
11
|
+
MutationEvent,
|
|
12
|
+
type ICreateEventMeta,
|
|
13
|
+
type IUpdateEventMeta,
|
|
14
|
+
type IRemoveEventMeta,
|
|
15
|
+
type IRestoreEventMeta,
|
|
16
|
+
type IDuplicateEventMeta,
|
|
17
|
+
} from '../events/mutation-event'
|
|
18
|
+
import type { IModelServiceArgs } from './model-service-args'
|
|
19
|
+
import { ReadOnlyModelService, type ILoadOptions } from './read-only-model-service'
|
|
20
|
+
import type { IActionOptions } from './base-model-service'
|
|
21
|
+
|
|
22
|
+
export interface ICreateOptions extends IActionOptions {
|
|
23
|
+
/**
|
|
24
|
+
* If true, skips dispatching events for this action.
|
|
25
|
+
*/
|
|
26
|
+
doNotDispatchEvents?: boolean
|
|
27
|
+
}
|
|
28
|
+
export interface IUpdateOptions extends IActionOptions {
|
|
29
|
+
/**
|
|
30
|
+
* If true, skips dispatching events for this action.
|
|
31
|
+
*/
|
|
32
|
+
doNotDispatchEvents?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface INormalizeInputArgs<TSchema extends AnyModelSchema> {
|
|
36
|
+
existing?: InferDetail<TSchema>
|
|
37
|
+
descriptor: ActionDescriptor
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelService<TSchema> {
|
|
41
|
+
constructor(args: IModelServiceArgs<TSchema>) {
|
|
42
|
+
super(args)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalizes input data before processing. This method can be overridden by subclasses
|
|
47
|
+
* to implement custom input normalization logic (e.g., trimming strings, setting defaults, etc.).
|
|
48
|
+
* By default, this method returns the input unchanged.
|
|
49
|
+
* @param input The input data to normalize.
|
|
50
|
+
* @returns The normalized input data.
|
|
51
|
+
*/
|
|
52
|
+
protected async normalizeInput(
|
|
53
|
+
input: InferInput<TSchema>,
|
|
54
|
+
args: INormalizeInputArgs<TSchema>,
|
|
55
|
+
): Promise<InferInput<TSchema>> {
|
|
56
|
+
return input
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Converts a detail object to a valid input for this service's schema.
|
|
61
|
+
* Picks only fields that exist in the input model and validates/coerces
|
|
62
|
+
* them through the input schema. Useful for duplicating entities or
|
|
63
|
+
* converting a detail from one entity type into an input for another.
|
|
64
|
+
* @param detail The detail object to convert (can be from any schema).
|
|
65
|
+
* @returns A validated input object with coerced values.
|
|
66
|
+
*/
|
|
67
|
+
async detailsToInput(detail: Record<string, unknown>): Promise<InferInput<TSchema>> {
|
|
68
|
+
const inputModel: IAnyModel = this.schema.definition.input
|
|
69
|
+
const inputJsonSchema = inputModel.toJSONSchema()
|
|
70
|
+
const inputFields = Object.keys(inputJsonSchema.properties ?? {})
|
|
71
|
+
|
|
72
|
+
// Pick only fields that exist in the input model
|
|
73
|
+
const picked: Record<string, unknown> = {}
|
|
74
|
+
for (const field of inputFields) {
|
|
75
|
+
if (field in detail) {
|
|
76
|
+
picked[field] = detail[field]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate through the input model to coerce values
|
|
81
|
+
const result = await inputModel.validate(picked as any)
|
|
82
|
+
|
|
83
|
+
if ('value' in result) {
|
|
84
|
+
return result.value as InferInput<TSchema>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return picked as InferInput<TSchema>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Duplicates an existing entity by loading it, converting to input, removing the
|
|
92
|
+
* primary key, and creating a new record. Accepts an optional partial input to
|
|
93
|
+
* merge on top of the converted copy before creation.
|
|
94
|
+
* @param lookup The lookup criteria to find the record to duplicate.
|
|
95
|
+
* @param overrides Optional partial input to merge on top of the duplicated data.
|
|
96
|
+
* @param options Optional create options.
|
|
97
|
+
* @returns The newly created duplicate record.
|
|
98
|
+
*/
|
|
99
|
+
async duplicate(
|
|
100
|
+
lookup: InferLookup<TSchema>,
|
|
101
|
+
overrides?: Partial<InferInput<TSchema>>,
|
|
102
|
+
options?: ICreateOptions,
|
|
103
|
+
): Promise<InferDetail<TSchema>> {
|
|
104
|
+
// Load the existing entity
|
|
105
|
+
const existing = await this.load(lookup)
|
|
106
|
+
if (!existing) {
|
|
107
|
+
throw new Error('Item not found')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Convert the detail to an input
|
|
111
|
+
const input = await this.detailsToInput(existing as Record<string, unknown>)
|
|
112
|
+
|
|
113
|
+
// Remove the primary key to ensure a new record gets created
|
|
114
|
+
if (this.entityMetadata?.primaryKey) {
|
|
115
|
+
delete (input as Record<string, unknown>)[this.entityMetadata.primaryKey]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Merge optional overrides
|
|
119
|
+
const finalInput = overrides ? Object.assign({}, input, overrides) : input
|
|
120
|
+
|
|
121
|
+
// Emit the before duplicate event
|
|
122
|
+
if (!options?.doNotDispatchEvents) {
|
|
123
|
+
const beforeDuplicateEvent = new MutationEvent<
|
|
124
|
+
InferDetail<TSchema>,
|
|
125
|
+
InferInput<TSchema>,
|
|
126
|
+
IDuplicateEventMeta<InferDetail<TSchema>, InferLookup<TSchema>, InferInput<TSchema>>
|
|
127
|
+
>(
|
|
128
|
+
this.getDescriptor(ModelMutationAction.BeforeDuplicate),
|
|
129
|
+
finalInput as InferInput<TSchema>,
|
|
130
|
+
).setMeta({ existing, args: { lookup, overrides, options: options as Record<string, unknown> } })
|
|
131
|
+
await this.emitter.emitAsync(beforeDuplicateEvent)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create the new record (also emits beforeCreate/afterCreate)
|
|
135
|
+
const result = await this.create(finalInput as InferInput<TSchema>, options)
|
|
136
|
+
|
|
137
|
+
// Emit the after duplicate event
|
|
138
|
+
if (!options?.doNotDispatchEvents) {
|
|
139
|
+
const afterDuplicateEvent = new MutationEvent<
|
|
140
|
+
InferDetail<TSchema>,
|
|
141
|
+
InferInput<TSchema>,
|
|
142
|
+
IDuplicateEventMeta<InferDetail<TSchema>, InferLookup<TSchema>, InferInput<TSchema>>
|
|
143
|
+
>(
|
|
144
|
+
this.getDescriptor(ModelMutationAction.AfterDuplicate),
|
|
145
|
+
finalInput as InferInput<TSchema>,
|
|
146
|
+
).setMeta({ existing, args: { lookup, overrides, options: options as Record<string, unknown> } }).setResult(result)
|
|
147
|
+
await this.emitter.emitAsync(afterDuplicateEvent)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Removes a record by its lookup criteria.
|
|
155
|
+
* @param lookup The lookup criteria to find the record.
|
|
156
|
+
* @returns The removed record.
|
|
157
|
+
*/
|
|
158
|
+
async remove(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferSummary<TSchema>> {
|
|
159
|
+
// Emit the before remove event
|
|
160
|
+
const beforeRemoveEvent = new MutationEvent<
|
|
161
|
+
InferSummary<TSchema>,
|
|
162
|
+
InferLookup<TSchema>,
|
|
163
|
+
IRemoveEventMeta<InferSummary<TSchema>, InferLookup<TSchema>>
|
|
164
|
+
>(
|
|
165
|
+
this.getDescriptor(ModelMutationAction.BeforeRemove),
|
|
166
|
+
lookup,
|
|
167
|
+
).setMeta({ args: { lookup, options: options as Record<string, unknown> } })
|
|
168
|
+
await this.emitter.emitAsync(beforeRemoveEvent)
|
|
169
|
+
|
|
170
|
+
// Perform the removal
|
|
171
|
+
const result = await this.repository.remove(lookup, options)
|
|
172
|
+
|
|
173
|
+
// Emit the after remove event
|
|
174
|
+
const afterRemoveEvent = new MutationEvent<
|
|
175
|
+
InferSummary<TSchema>,
|
|
176
|
+
InferLookup<TSchema>,
|
|
177
|
+
IRemoveEventMeta<InferSummary<TSchema>, InferLookup<TSchema>>
|
|
178
|
+
>(
|
|
179
|
+
this.getDescriptor(ModelMutationAction.AfterRemove),
|
|
180
|
+
lookup,
|
|
181
|
+
).setMeta({ args: { lookup, options: options as Record<string, unknown> } }).setResult(result)
|
|
182
|
+
await this.emitter.emitAsync(afterRemoveEvent)
|
|
183
|
+
|
|
184
|
+
// Return the results of the removal
|
|
185
|
+
return await this.normalizeSummary(result)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Restores a record by its lookup criteria.
|
|
190
|
+
* If a soft-deleted copy exists, it will be restored.
|
|
191
|
+
* @param lookup The lookup criteria to find the record to restore.
|
|
192
|
+
* @returns
|
|
193
|
+
*/
|
|
194
|
+
async restore(lookup: InferLookup<TSchema>, options?: ILoadOptions): Promise<InferSummary<TSchema>> {
|
|
195
|
+
// Emit the before restore event
|
|
196
|
+
const beforeRestoreEvent = new MutationEvent<
|
|
197
|
+
InferSummary<TSchema>,
|
|
198
|
+
InferLookup<TSchema>,
|
|
199
|
+
IRestoreEventMeta<InferSummary<TSchema>, InferLookup<TSchema>>
|
|
200
|
+
>(
|
|
201
|
+
this.getDescriptor(ModelMutationAction.BeforeRestore),
|
|
202
|
+
lookup,
|
|
203
|
+
).setMeta({ args: { lookup, options: options as Record<string, unknown> } })
|
|
204
|
+
await this.emitter.emitAsync(beforeRestoreEvent)
|
|
205
|
+
|
|
206
|
+
// Perform the restore operation
|
|
207
|
+
const result = await this.repository.restore(lookup, options)
|
|
208
|
+
|
|
209
|
+
// Emit the after restore event
|
|
210
|
+
const afterRestoreEvent = new MutationEvent<
|
|
211
|
+
InferSummary<TSchema>,
|
|
212
|
+
InferLookup<TSchema>,
|
|
213
|
+
IRestoreEventMeta<InferSummary<TSchema>, InferLookup<TSchema>>
|
|
214
|
+
>(
|
|
215
|
+
this.getDescriptor(ModelMutationAction.AfterRestore),
|
|
216
|
+
lookup,
|
|
217
|
+
).setMeta({ args: { lookup, options: options as Record<string, unknown> } }).setResult(result)
|
|
218
|
+
await this.emitter.emitAsync(afterRestoreEvent)
|
|
219
|
+
|
|
220
|
+
// Return the results of the restore operation
|
|
221
|
+
return await this.normalizeSummary(result)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async create(input: InferInput<TSchema>, options?: ICreateOptions): Promise<InferDetail<TSchema>> {
|
|
225
|
+
// Normalize the input data
|
|
226
|
+
const normalizedInput = await this.normalizeInput(input, {
|
|
227
|
+
descriptor: this.getDescriptor(ModelMutationAction.Create),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// Emit the before create event
|
|
231
|
+
if (!options?.doNotDispatchEvents) {
|
|
232
|
+
const beforeCreateEvent = new MutationEvent<
|
|
233
|
+
InferDetail<TSchema>,
|
|
234
|
+
InferInput<TSchema>,
|
|
235
|
+
ICreateEventMeta<InferDetail<TSchema>, InferInput<TSchema>>
|
|
236
|
+
>(
|
|
237
|
+
this.getDescriptor(ModelMutationAction.BeforeCreate),
|
|
238
|
+
normalizedInput,
|
|
239
|
+
).setMeta({ args: { input: normalizedInput, options: options as Record<string, unknown> } })
|
|
240
|
+
await this.emitter.emitAsync(beforeCreateEvent)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Perform the creation
|
|
244
|
+
const result = await this.repository.create(normalizedInput, options)
|
|
245
|
+
|
|
246
|
+
// Emit the after create event
|
|
247
|
+
if (!options?.doNotDispatchEvents) {
|
|
248
|
+
const afterCreateEvent = new MutationEvent<
|
|
249
|
+
InferDetail<TSchema>,
|
|
250
|
+
InferInput<TSchema>,
|
|
251
|
+
ICreateEventMeta<InferDetail<TSchema>, InferInput<TSchema>>
|
|
252
|
+
>(
|
|
253
|
+
this.getDescriptor(ModelMutationAction.AfterCreate),
|
|
254
|
+
normalizedInput,
|
|
255
|
+
).setMeta({ args: { input: normalizedInput, options: options as Record<string, unknown> } }).setResult(result)
|
|
256
|
+
await this.emitter.emitAsync(afterCreateEvent)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Return the results of the creation
|
|
260
|
+
return await this.normalizeDetail(result)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async update(
|
|
264
|
+
lookup: InferLookup<TSchema>,
|
|
265
|
+
input: InferInput<TSchema>,
|
|
266
|
+
options?: IUpdateOptions,
|
|
267
|
+
): Promise<InferDetail<TSchema>> {
|
|
268
|
+
const existing = await this.repository.load(lookup, { ...options, doNotDispatchEvents: true })
|
|
269
|
+
// Normalize the input data
|
|
270
|
+
const normalizedInput = await this.normalizeInput(input, {
|
|
271
|
+
existing,
|
|
272
|
+
descriptor: this.getDescriptor(ModelMutationAction.Update),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Emit the before update event
|
|
276
|
+
if (!options?.doNotDispatchEvents) {
|
|
277
|
+
const beforeUpdateEvent = new MutationEvent<
|
|
278
|
+
InferDetail<TSchema>,
|
|
279
|
+
InferInput<TSchema>,
|
|
280
|
+
IUpdateEventMeta<InferDetail<TSchema>, InferLookup<TSchema>, InferInput<TSchema>>
|
|
281
|
+
>(
|
|
282
|
+
this.getDescriptor(ModelMutationAction.BeforeUpdate),
|
|
283
|
+
normalizedInput,
|
|
284
|
+
).setMeta({ existing, args: { lookup, input, options: options as Record<string, unknown> } })
|
|
285
|
+
await this.emitter.emitAsync(beforeUpdateEvent)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Perform the update
|
|
289
|
+
const result = await this.repository.update(lookup, normalizedInput, options)
|
|
290
|
+
|
|
291
|
+
// Emit the after update event
|
|
292
|
+
if (!options?.doNotDispatchEvents) {
|
|
293
|
+
const afterUpdateEvent = new MutationEvent<
|
|
294
|
+
InferDetail<TSchema>,
|
|
295
|
+
InferInput<TSchema>,
|
|
296
|
+
IUpdateEventMeta<InferDetail<TSchema>, InferLookup<TSchema>, InferInput<TSchema>>
|
|
297
|
+
>(
|
|
298
|
+
this.getDescriptor(ModelMutationAction.AfterUpdate),
|
|
299
|
+
normalizedInput,
|
|
300
|
+
).setMeta({ existing, args: { lookup, input, options: options as Record<string, unknown> } }).setResult(result)
|
|
301
|
+
await this.emitter.emitAsync(afterUpdateEvent)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Return the results of the update
|
|
305
|
+
return await this.normalizeDetail(result)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Upserts a record (creates if it doesn't exist, updates if it does).
|
|
310
|
+
* @param input The input data for the upsert operation.
|
|
311
|
+
* @param options Optional create or update options.
|
|
312
|
+
* @returns The upserted record.
|
|
313
|
+
*/
|
|
314
|
+
async upsert(input: InferInput<TSchema>, options?: ICreateOptions | IUpdateOptions): Promise<InferDetail<TSchema>> {
|
|
315
|
+
const primaryKeyValue = this.getPrimaryKeyValue(input)
|
|
316
|
+
|
|
317
|
+
let operation: ModelMutationAction
|
|
318
|
+
let beforeOperation: ModelMutationAction
|
|
319
|
+
let afterOperation: ModelMutationAction
|
|
320
|
+
let existingItem: InferDetail<TSchema> | undefined = undefined
|
|
321
|
+
|
|
322
|
+
if (primaryKeyValue === undefined) {
|
|
323
|
+
operation = ModelMutationAction.Create
|
|
324
|
+
beforeOperation = ModelMutationAction.BeforeCreate
|
|
325
|
+
afterOperation = ModelMutationAction.AfterCreate
|
|
326
|
+
} else {
|
|
327
|
+
existingItem = await this.load(
|
|
328
|
+
{
|
|
329
|
+
[this.entityMetadata.primaryKey]: primaryKeyValue,
|
|
330
|
+
} as InferLookup<TSchema>,
|
|
331
|
+
{
|
|
332
|
+
...options,
|
|
333
|
+
doNotDispatchEvents: true,
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if (existingItem) {
|
|
338
|
+
operation = ModelMutationAction.Update
|
|
339
|
+
beforeOperation = ModelMutationAction.BeforeUpdate
|
|
340
|
+
afterOperation = ModelMutationAction.AfterUpdate
|
|
341
|
+
} else {
|
|
342
|
+
operation = ModelMutationAction.Create
|
|
343
|
+
beforeOperation = ModelMutationAction.BeforeCreate
|
|
344
|
+
afterOperation = ModelMutationAction.AfterCreate
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Normalize the input data
|
|
349
|
+
const normalizedInput = await this.normalizeInput(input, {
|
|
350
|
+
descriptor: this.getDescriptor(operation),
|
|
351
|
+
existing: existingItem,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// Emit the before upsert event
|
|
355
|
+
if (!options?.doNotDispatchEvents) {
|
|
356
|
+
const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
|
|
357
|
+
this.getDescriptor(beforeOperation),
|
|
358
|
+
normalizedInput,
|
|
359
|
+
)
|
|
360
|
+
await this.emitter.emitAsync(beforeUpsertEvent)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Perform the upsert operation
|
|
364
|
+
const result = await this.repository.upsert(normalizedInput, options)
|
|
365
|
+
|
|
366
|
+
// Emit the after upsert event
|
|
367
|
+
if (!options?.doNotDispatchEvents) {
|
|
368
|
+
const afterUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
|
|
369
|
+
this.getDescriptor(afterOperation),
|
|
370
|
+
normalizedInput,
|
|
371
|
+
).setResult(result)
|
|
372
|
+
await this.emitter.emitAsync(afterUpsertEvent)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Return the results of the upsert operation
|
|
376
|
+
return await this.normalizeDetail(result)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Bulk upserts multiple records (creates if they don't exist, updates if they do).
|
|
381
|
+
* @param inputs Array of input data for the bulk upsert operation.
|
|
382
|
+
* @param options Optional create or update options.
|
|
383
|
+
* @returns Array of upserted records.
|
|
384
|
+
*/
|
|
385
|
+
async bulkUpsert(
|
|
386
|
+
inputs: InferInput<TSchema>[],
|
|
387
|
+
options?: ICreateOptions | IUpdateOptions,
|
|
388
|
+
): Promise<InferDetail<TSchema>[]> {
|
|
389
|
+
if (inputs.length === 0) {
|
|
390
|
+
return []
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Keep track of input metadata for each position (preserves order and duplicates)
|
|
394
|
+
type InputInfo = {
|
|
395
|
+
input: InferInput<TSchema>
|
|
396
|
+
index: number
|
|
397
|
+
primaryKeyValue?: string | number
|
|
398
|
+
existingEntity?: InferDetail<TSchema>
|
|
399
|
+
operation?: ModelMutationAction
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const inputInfos: InputInfo[] = []
|
|
403
|
+
const uniqueLookups = new Map<string | number, InferLookup<TSchema>>()
|
|
404
|
+
|
|
405
|
+
// Process each input and collect unique lookups
|
|
406
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
407
|
+
const input = inputs[i]
|
|
408
|
+
const primaryKeyValue = this.getPrimaryKeyValue(input)
|
|
409
|
+
|
|
410
|
+
const inputInfo: InputInfo = {
|
|
411
|
+
input,
|
|
412
|
+
index: i,
|
|
413
|
+
primaryKeyValue,
|
|
414
|
+
}
|
|
415
|
+
inputInfos.push(inputInfo)
|
|
416
|
+
|
|
417
|
+
// Collect unique lookups for entities that have primary keys
|
|
418
|
+
if (primaryKeyValue !== undefined) {
|
|
419
|
+
uniqueLookups.set(primaryKeyValue, {
|
|
420
|
+
[this.entityMetadata.primaryKey]: primaryKeyValue,
|
|
421
|
+
} as InferLookup<TSchema>)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Load existing entities for unique primary keys
|
|
426
|
+
const existingEntitiesMap = new Map<string | number, InferDetail<TSchema>>()
|
|
427
|
+
if (uniqueLookups.size > 0) {
|
|
428
|
+
const lookups = Array.from(uniqueLookups.values())
|
|
429
|
+
const existingEntities = await this.loadMany(lookups, {
|
|
430
|
+
...options,
|
|
431
|
+
doNotDispatchEvents: true,
|
|
432
|
+
})
|
|
433
|
+
existingEntities.forEach((entity) => {
|
|
434
|
+
if (entity) {
|
|
435
|
+
const pkValue = this.getPrimaryKeyValue(entity)
|
|
436
|
+
if (pkValue !== undefined) {
|
|
437
|
+
existingEntitiesMap.set(pkValue, entity)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Normalize all inputs and determine operations in parallel
|
|
444
|
+
const normalizationPromises = inputInfos.map(async (inputInfo) => {
|
|
445
|
+
// Set existing entity if found
|
|
446
|
+
if (inputInfo.primaryKeyValue !== undefined) {
|
|
447
|
+
inputInfo.existingEntity = existingEntitiesMap.get(inputInfo.primaryKeyValue)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Determine operation type
|
|
451
|
+
inputInfo.operation = inputInfo.existingEntity
|
|
452
|
+
? ModelMutationAction.BeforeUpdate
|
|
453
|
+
: ModelMutationAction.BeforeCreate
|
|
454
|
+
|
|
455
|
+
// Normalize the input
|
|
456
|
+
const normalizedInput = await this.normalizeInput(inputInfo.input, {
|
|
457
|
+
existing: inputInfo.existingEntity,
|
|
458
|
+
descriptor: this.getDescriptor(
|
|
459
|
+
inputInfo.existingEntity ? ModelMutationAction.Update : ModelMutationAction.Create,
|
|
460
|
+
),
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
inputInfo.input = normalizedInput
|
|
464
|
+
return normalizedInput
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const normalizedInputs = await Promise.all(normalizationPromises)
|
|
468
|
+
|
|
469
|
+
// Create before events
|
|
470
|
+
if (!options?.doNotDispatchEvents) {
|
|
471
|
+
const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
|
|
472
|
+
for (const inputInfo of inputInfos) {
|
|
473
|
+
beforeEvents.push(
|
|
474
|
+
new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
|
|
475
|
+
this.getDescriptor(inputInfo.operation!),
|
|
476
|
+
inputInfo.input,
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Emit all before events
|
|
482
|
+
await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Perform the bulk upsert operation with all normalized inputs
|
|
486
|
+
const results = await this.repository.bulkUpsert(normalizedInputs, options)
|
|
487
|
+
|
|
488
|
+
// Create after events and return results
|
|
489
|
+
if (!options?.doNotDispatchEvents) {
|
|
490
|
+
const afterEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
|
|
491
|
+
|
|
492
|
+
for (let i = 0; i < inputInfos.length; i++) {
|
|
493
|
+
const inputInfo = inputInfos[i]
|
|
494
|
+
const result = results[i]
|
|
495
|
+
|
|
496
|
+
const afterOperation =
|
|
497
|
+
inputInfo.operation === ModelMutationAction.BeforeCreate
|
|
498
|
+
? ModelMutationAction.AfterCreate
|
|
499
|
+
: ModelMutationAction.AfterUpdate
|
|
500
|
+
|
|
501
|
+
afterEvents.push(
|
|
502
|
+
new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
|
|
503
|
+
this.getDescriptor(afterOperation),
|
|
504
|
+
inputInfo.input,
|
|
505
|
+
).setResult(result),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Emit all after events
|
|
510
|
+
await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Return normalized results
|
|
514
|
+
return await Promise.all(results.map((result) => this.normalizeDetail(result)))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Permanently deletes all items from trash, optionally filtered by the provided criteria.
|
|
519
|
+
* @param filters Optional filters to apply when selecting items to delete from trash.
|
|
520
|
+
* @returns The count of permanently deleted items.
|
|
521
|
+
*/
|
|
522
|
+
async emptyTrash(filters?: InferFilters<TSchema>): Promise<number> {
|
|
523
|
+
// Emit the before empty trash event
|
|
524
|
+
const beforeEmptyTrashEvent = new MutationEvent<number, InferFilters<TSchema> | undefined>(
|
|
525
|
+
this.getDescriptor(ModelMutationAction.BeforeEmptyTrash),
|
|
526
|
+
filters,
|
|
527
|
+
)
|
|
528
|
+
await this.emitter.emitAsync(beforeEmptyTrashEvent)
|
|
529
|
+
|
|
530
|
+
// Perform the empty trash operation
|
|
531
|
+
const count = await this.repository.emptyTrash(filters)
|
|
532
|
+
|
|
533
|
+
// Emit the after empty trash event
|
|
534
|
+
const afterEmptyTrashEvent = new MutationEvent<number, InferFilters<TSchema> | undefined>(
|
|
535
|
+
this.getDescriptor(ModelMutationAction.AfterEmptyTrash),
|
|
536
|
+
filters,
|
|
537
|
+
).setResult(count)
|
|
538
|
+
await this.emitter.emitAsync(afterEmptyTrashEvent)
|
|
539
|
+
|
|
540
|
+
// Return the count of deleted items
|
|
541
|
+
return count
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Permanently deletes a specific item from trash based on the provided lookup.
|
|
546
|
+
* @param lookup The lookup criteria for the item to permanently delete from trash.
|
|
547
|
+
* @returns The permanently deleted item summary.
|
|
548
|
+
*/
|
|
549
|
+
async permanentlyDeleteFromTrash(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
|
|
550
|
+
// Emit the before permanently delete from trash event
|
|
551
|
+
const beforePermanentlyDeleteFromTrashEvent = new MutationEvent<InferSummary<TSchema>, InferLookup<TSchema>>(
|
|
552
|
+
this.getDescriptor(ModelMutationAction.BeforePermanentlyDeleteFromTrash),
|
|
553
|
+
lookup,
|
|
554
|
+
)
|
|
555
|
+
await this.emitter.emitAsync(beforePermanentlyDeleteFromTrashEvent)
|
|
556
|
+
|
|
557
|
+
// Perform the permanent deletion from trash
|
|
558
|
+
const result = await this.repository.permanentlyDeleteFromTrash(lookup)
|
|
559
|
+
|
|
560
|
+
// Emit the after permanently delete from trash event
|
|
561
|
+
const afterPermanentlyDeleteFromTrashEvent = new MutationEvent<InferSummary<TSchema>, InferLookup<TSchema>>(
|
|
562
|
+
this.getDescriptor(ModelMutationAction.AfterPermanentlyDeleteFromTrash),
|
|
563
|
+
lookup,
|
|
564
|
+
).setResult(result)
|
|
565
|
+
await this.emitter.emitAsync(afterPermanentlyDeleteFromTrashEvent)
|
|
566
|
+
|
|
567
|
+
// Return the results of the permanent deletion
|
|
568
|
+
return await this.normalizeSummary(result)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Permanently deletes an item based on the provided lookup, regardless of whether it is active or in trash.
|
|
573
|
+
* @param lookup The lookup criteria for the item to permanently delete.
|
|
574
|
+
* @returns The permanently deleted item summary.
|
|
575
|
+
*/
|
|
576
|
+
async permanentlyDelete(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
|
|
577
|
+
// Emit the before permanently delete event
|
|
578
|
+
const beforePermanentlyDeleteEvent = new MutationEvent<InferSummary<TSchema>, InferLookup<TSchema>>(
|
|
579
|
+
this.getDescriptor(ModelMutationAction.BeforePermanentlyDelete),
|
|
580
|
+
lookup,
|
|
581
|
+
)
|
|
582
|
+
await this.emitter.emitAsync(beforePermanentlyDeleteEvent)
|
|
583
|
+
|
|
584
|
+
// Perform the permanent deletion
|
|
585
|
+
const result = await this.repository.permanentlyDelete(lookup)
|
|
586
|
+
|
|
587
|
+
// Emit the after permanently delete event
|
|
588
|
+
const afterPermanentlyDeleteEvent = new MutationEvent<InferSummary<TSchema>, InferLookup<TSchema>>(
|
|
589
|
+
this.getDescriptor(ModelMutationAction.AfterPermanentlyDelete),
|
|
590
|
+
lookup,
|
|
591
|
+
).setResult(result)
|
|
592
|
+
await this.emitter.emitAsync(afterPermanentlyDeleteEvent)
|
|
593
|
+
|
|
594
|
+
// Return the results of the permanent deletion
|
|
595
|
+
return await this.normalizeSummary(result)
|
|
596
|
+
}
|
|
597
|
+
}
|