@declaro/data 2.0.0-beta.13 → 2.0.0-beta.131

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