@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.
Files changed (142) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +0 -0
  3. package/dist/browser/index.js +26 -0
  4. package/dist/browser/index.js.map +93 -0
  5. package/dist/node/index.cjs +13372 -0
  6. package/dist/node/index.cjs.map +93 -0
  7. package/dist/node/index.js +13351 -0
  8. package/dist/node/index.js.map +93 -0
  9. package/dist/ts/application/model-controller.d.ts +60 -0
  10. package/dist/ts/application/model-controller.d.ts.map +1 -0
  11. package/dist/ts/application/model-controller.test.d.ts +2 -0
  12. package/dist/ts/application/model-controller.test.d.ts.map +1 -0
  13. package/dist/ts/application/read-only-model-controller.d.ts +24 -0
  14. package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
  15. package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
  16. package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
  17. package/dist/ts/domain/events/domain-event.d.ts +41 -0
  18. package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
  19. package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
  20. package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
  21. package/dist/ts/domain/events/event-types.d.ts +37 -0
  22. package/dist/ts/domain/events/event-types.d.ts.map +1 -0
  23. package/dist/ts/domain/events/mutation-event.d.ts +41 -0
  24. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
  25. package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
  26. package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
  27. package/dist/ts/domain/events/query-event.d.ts +8 -0
  28. package/dist/ts/domain/events/query-event.d.ts.map +1 -0
  29. package/dist/ts/domain/events/query-event.test.d.ts +2 -0
  30. package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
  31. package/dist/ts/domain/events/request-event.d.ts +26 -0
  32. package/dist/ts/domain/events/request-event.d.ts.map +1 -0
  33. package/dist/ts/domain/events/request-event.test.d.ts +2 -0
  34. package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
  35. package/dist/ts/domain/interfaces/repository.d.ts +110 -0
  36. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
  37. package/dist/ts/domain/models/pagination.d.ts +28 -0
  38. package/dist/ts/domain/models/pagination.d.ts.map +1 -0
  39. package/dist/ts/domain/services/base-model-service.d.ts +23 -0
  40. package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
  41. package/dist/ts/domain/services/model-service-args.d.ts +9 -0
  42. package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
  43. package/dist/ts/domain/services/model-service.d.ts +99 -0
  44. package/dist/ts/domain/services/model-service.d.ts.map +1 -0
  45. package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
  46. package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
  47. package/dist/ts/domain/services/model-service.test.d.ts +2 -0
  48. package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
  49. package/dist/ts/domain/services/read-only-model-service.d.ts +90 -0
  50. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
  51. package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
  52. package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
  53. package/dist/ts/index.d.ts +18 -0
  54. package/dist/ts/index.d.ts.map +1 -0
  55. package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
  56. package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
  57. package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
  58. package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
  59. package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
  60. package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
  61. package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
  62. package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
  63. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
  64. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
  65. package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
  66. package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
  67. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
  68. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
  69. package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
  70. package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
  71. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  72. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  73. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  74. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  75. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  76. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  77. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  78. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  79. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
  80. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
  81. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  82. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  83. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  84. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  85. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  86. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  87. package/package.json +46 -42
  88. package/src/application/model-controller.test.ts +694 -0
  89. package/src/application/model-controller.ts +186 -0
  90. package/src/application/read-only-model-controller.test.ts +335 -0
  91. package/src/application/read-only-model-controller.ts +79 -0
  92. package/src/domain/events/domain-event.test.ts +82 -0
  93. package/src/domain/events/domain-event.ts +69 -0
  94. package/src/domain/events/event-types.ts +37 -0
  95. package/src/domain/events/mutation-event.test.ts +390 -0
  96. package/src/domain/events/mutation-event.ts +53 -0
  97. package/src/domain/events/query-event.test.ts +228 -0
  98. package/src/domain/events/query-event.ts +14 -0
  99. package/src/domain/events/request-event.test.ts +38 -0
  100. package/src/domain/events/request-event.ts +47 -0
  101. package/src/domain/interfaces/repository.ts +136 -0
  102. package/src/domain/models/pagination.ts +28 -0
  103. package/src/domain/services/base-model-service.ts +54 -0
  104. package/src/domain/services/model-service-args.ts +9 -0
  105. package/src/domain/services/model-service.normalization.test.ts +704 -0
  106. package/src/domain/services/model-service.test.ts +1616 -0
  107. package/src/domain/services/model-service.ts +597 -0
  108. package/src/domain/services/read-only-model-service.test.ts +1130 -0
  109. package/src/domain/services/read-only-model-service.ts +211 -0
  110. package/src/index.ts +17 -4
  111. package/src/shared/utils/schema-inference.ts +26 -0
  112. package/src/shared/utils/schema-inheritance.test.ts +295 -0
  113. package/src/shared/utils/schema-inheritance.ts +28 -0
  114. package/src/shared/utils/test/animal-schema.ts +46 -0
  115. package/src/shared/utils/test/animal-trait-schema.ts +45 -0
  116. package/src/shared/utils/test/elephant-schema.ts +58 -0
  117. package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
  118. package/src/test/mock/models/mock-book-models.ts +78 -0
  119. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  120. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  121. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  122. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  123. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  124. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  125. package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
  126. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  127. package/dist/databaseConnection.d.ts +0 -24
  128. package/dist/datastoreAbstract.d.ts +0 -37
  129. package/dist/declaro-data.cjs +0 -1
  130. package/dist/declaro-data.mjs +0 -250
  131. package/dist/hydrateEntity.d.ts +0 -8
  132. package/dist/index.d.ts +0 -4
  133. package/dist/serverConnection.d.ts +0 -15
  134. package/dist/trackedStatus.d.ts +0 -15
  135. package/src/databaseConnection.ts +0 -137
  136. package/src/datastoreAbstract.ts +0 -190
  137. package/src/hydrateEntity.ts +0 -36
  138. package/src/placeholder.test.ts +0 -7
  139. package/src/serverConnection.ts +0 -74
  140. package/src/trackedStatus.ts +0 -35
  141. package/tsconfig.json +0 -10
  142. 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
+ }