@declaro/data 2.0.0-beta.116 → 2.0.0-beta.118

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.
@@ -1,4 +1,4 @@
1
- import type { AnyModelSchema } from '@declaro/core'
1
+ import type { ActionDescriptor, AnyModelSchema, IActionDescriptor } from '@declaro/core'
2
2
  import type { InferDetail, InferInput, InferLookup, InferSummary } from '../../shared/utils/schema-inference'
3
3
  import { ModelMutationAction, ModelQueryEvent } from '../events/event-types'
4
4
  import { MutationEvent } from '../events/mutation-event'
@@ -9,6 +9,11 @@ import type { IActionOptions } from './base-model-service'
9
9
  export interface ICreateOptions extends IActionOptions {}
10
10
  export interface IUpdateOptions extends IActionOptions {}
11
11
 
12
+ export interface INormalizeInputArgs<TSchema extends AnyModelSchema> {
13
+ existing?: InferDetail<TSchema>
14
+ descriptor: ActionDescriptor
15
+ }
16
+
12
17
  export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelService<TSchema> {
13
18
  constructor(args: IModelServiceArgs<TSchema>) {
14
19
  super(args)
@@ -21,7 +26,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
21
26
  * @param input The input data to normalize.
22
27
  * @returns The normalized input data.
23
28
  */
24
- protected async normalizeInput(input: InferInput<TSchema>): Promise<InferInput<TSchema>> {
29
+ protected async normalizeInput(
30
+ input: InferInput<TSchema>,
31
+ args: INormalizeInputArgs<TSchema>,
32
+ ): Promise<InferInput<TSchema>> {
25
33
  return input
26
34
  }
27
35
 
@@ -82,7 +90,9 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
82
90
 
83
91
  async create(input: InferInput<TSchema>, options?: ICreateOptions): Promise<InferDetail<TSchema>> {
84
92
  // Normalize the input data
85
- const normalizedInput = await this.normalizeInput(input)
93
+ const normalizedInput = await this.normalizeInput(input, {
94
+ descriptor: this.getDescriptor(ModelMutationAction.Create),
95
+ })
86
96
 
87
97
  // Emit the before create event
88
98
  const beforeCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
@@ -110,8 +120,12 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
110
120
  input: InferInput<TSchema>,
111
121
  options?: IUpdateOptions,
112
122
  ): Promise<InferDetail<TSchema>> {
123
+ const existing = await this.repository.load(lookup, options)
113
124
  // Normalize the input data
114
- const normalizedInput = await this.normalizeInput(input)
125
+ const normalizedInput = await this.normalizeInput(input, {
126
+ existing,
127
+ descriptor: this.getDescriptor(ModelMutationAction.Update),
128
+ })
115
129
 
116
130
  // Emit the before update event
117
131
  const beforeUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
@@ -141,19 +155,19 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
141
155
  * @returns The upserted record.
142
156
  */
143
157
  async upsert(input: InferInput<TSchema>, options?: ICreateOptions | IUpdateOptions): Promise<InferDetail<TSchema>> {
144
- // Normalize the input data
145
- const normalizedInput = await this.normalizeInput(input)
146
-
147
- const primaryKeyValue = this.getPrimaryKeyValue(normalizedInput)
158
+ const primaryKeyValue = this.getPrimaryKeyValue(input)
148
159
 
160
+ let operation: ModelMutationAction
149
161
  let beforeOperation: ModelMutationAction
150
162
  let afterOperation: ModelMutationAction
163
+ let existingItem: InferDetail<TSchema> | undefined = undefined
151
164
 
152
165
  if (primaryKeyValue === undefined) {
166
+ operation = ModelMutationAction.Create
153
167
  beforeOperation = ModelMutationAction.BeforeCreate
154
168
  afterOperation = ModelMutationAction.AfterCreate
155
169
  } else {
156
- const existingItem = await this.load(
170
+ existingItem = await this.load(
157
171
  {
158
172
  [this.entityMetadata.primaryKey]: primaryKeyValue,
159
173
  } as InferLookup<TSchema>,
@@ -161,14 +175,22 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
161
175
  )
162
176
 
163
177
  if (existingItem) {
178
+ operation = ModelMutationAction.Update
164
179
  beforeOperation = ModelMutationAction.BeforeUpdate
165
180
  afterOperation = ModelMutationAction.AfterUpdate
166
181
  } else {
182
+ operation = ModelMutationAction.Create
167
183
  beforeOperation = ModelMutationAction.BeforeCreate
168
184
  afterOperation = ModelMutationAction.AfterCreate
169
185
  }
170
186
  }
171
187
 
188
+ // Normalize the input data
189
+ const normalizedInput = await this.normalizeInput(input, {
190
+ descriptor: this.getDescriptor(operation),
191
+ existing: existingItem,
192
+ })
193
+
172
194
  // Emit the before upsert event
173
195
  const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
174
196
  this.getDescriptor(beforeOperation),
@@ -204,82 +226,86 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
204
226
  return []
205
227
  }
206
228
 
207
- // Normalize all input data in parallel using Promise.all
208
- const normalizedInputs = await Promise.all(inputs.map((input) => this.normalizeInput(input)))
209
-
210
- // Build a map of primary key to input and lookup info
211
- type EntityInfo = {
229
+ // Keep track of input metadata for each position (preserves order and duplicates)
230
+ type InputInfo = {
212
231
  input: InferInput<TSchema>
213
- lookup: InferLookup<TSchema>
214
- primaryKeyValue: string | number
232
+ index: number
233
+ primaryKeyValue?: string | number
215
234
  existingEntity?: InferDetail<TSchema>
216
235
  operation?: ModelMutationAction
217
236
  }
218
237
 
219
- const entityInfoMap = new Map<string | number, EntityInfo>()
220
- const inputsWithoutPrimaryKey: InferInput<TSchema>[] = []
238
+ const inputInfos: InputInfo[] = []
239
+ const uniqueLookups = new Map<string | number, InferLookup<TSchema>>()
221
240
 
222
- // Process each normalized input and organize by primary key
223
- for (const input of normalizedInputs) {
241
+ // Process each input and collect unique lookups
242
+ for (let i = 0; i < inputs.length; i++) {
243
+ const input = inputs[i]
224
244
  const primaryKeyValue = this.getPrimaryKeyValue(input)
225
245
 
246
+ const inputInfo: InputInfo = {
247
+ input,
248
+ index: i,
249
+ primaryKeyValue,
250
+ }
251
+ inputInfos.push(inputInfo)
252
+
253
+ // Collect unique lookups for entities that have primary keys
226
254
  if (primaryKeyValue !== undefined) {
227
- const entityInfo: EntityInfo = {
228
- input,
229
- primaryKeyValue,
230
- lookup: {
231
- [this.entityMetadata.primaryKey]: primaryKeyValue,
232
- } as InferLookup<TSchema>,
233
- }
234
- entityInfoMap.set(primaryKeyValue, entityInfo)
235
- } else {
236
- // Inputs without primary keys are always creates
237
- inputsWithoutPrimaryKey.push(input)
255
+ uniqueLookups.set(primaryKeyValue, {
256
+ [this.entityMetadata.primaryKey]: primaryKeyValue,
257
+ } as InferLookup<TSchema>)
238
258
  }
239
259
  }
240
260
 
241
- // Extract lookups for existing entities
242
- const lookups = Array.from(entityInfoMap.values()).map((info) => info.lookup)
243
-
244
- // Load existing entities and update the map
245
- if (lookups.length > 0) {
261
+ // Load existing entities for unique primary keys
262
+ const existingEntitiesMap = new Map<string | number, InferDetail<TSchema>>()
263
+ if (uniqueLookups.size > 0) {
264
+ const lookups = Array.from(uniqueLookups.values())
246
265
  const existingEntities = await this.loadMany(lookups, options)
247
266
  existingEntities.forEach((entity) => {
248
267
  if (entity) {
249
268
  const pkValue = this.getPrimaryKeyValue(entity)
250
- if (pkValue !== undefined && entityInfoMap.has(pkValue)) {
251
- const entityInfo = entityInfoMap.get(pkValue)!
252
- entityInfo.existingEntity = entity
269
+ if (pkValue !== undefined) {
270
+ existingEntitiesMap.set(pkValue, entity)
253
271
  }
254
272
  }
255
273
  })
256
274
  }
257
275
 
258
- // Determine operation types and prepare before events
259
- const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
276
+ // Normalize all inputs and determine operations in parallel
277
+ const normalizationPromises = inputInfos.map(async (inputInfo) => {
278
+ // Set existing entity if found
279
+ if (inputInfo.primaryKeyValue !== undefined) {
280
+ inputInfo.existingEntity = existingEntitiesMap.get(inputInfo.primaryKeyValue)
281
+ }
260
282
 
261
- // Handle entities with primary keys
262
- for (const entityInfo of entityInfoMap.values()) {
263
- const operation = entityInfo.existingEntity
283
+ // Determine operation type
284
+ inputInfo.operation = inputInfo.existingEntity
264
285
  ? ModelMutationAction.BeforeUpdate
265
286
  : ModelMutationAction.BeforeCreate
266
287
 
267
- entityInfo.operation = operation
268
-
269
- beforeEvents.push(
270
- new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
271
- this.getDescriptor(operation),
272
- entityInfo.input,
288
+ // Normalize the input
289
+ const normalizedInput = await this.normalizeInput(inputInfo.input, {
290
+ existing: inputInfo.existingEntity,
291
+ descriptor: this.getDescriptor(
292
+ inputInfo.existingEntity ? ModelMutationAction.Update : ModelMutationAction.Create,
273
293
  ),
274
- )
275
- }
294
+ })
295
+
296
+ inputInfo.input = normalizedInput
297
+ return normalizedInput
298
+ })
276
299
 
277
- // Handle inputs without primary keys (always creates)
278
- for (const input of inputsWithoutPrimaryKey) {
300
+ const normalizedInputs = await Promise.all(normalizationPromises)
301
+
302
+ // Create before events
303
+ const beforeEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
304
+ for (const inputInfo of inputInfos) {
279
305
  beforeEvents.push(
280
306
  new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
281
- this.getDescriptor(ModelMutationAction.BeforeCreate),
282
- input,
307
+ this.getDescriptor(inputInfo.operation!),
308
+ inputInfo.input,
283
309
  ),
284
310
  )
285
311
  }
@@ -287,59 +313,33 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
287
313
  // Emit all before events
288
314
  await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
289
315
 
290
- // Perform the bulk upsert operation with normalized inputs
316
+ // Perform the bulk upsert operation with all normalized inputs
291
317
  const results = await this.repository.bulkUpsert(normalizedInputs, options)
292
318
 
293
- // Create a map of result primary keys to results for matching
294
- const resultsByPrimaryKey = new Map<string | number, InferDetail<TSchema>>()
295
- const resultsWithoutPrimaryKey: InferDetail<TSchema>[] = []
296
-
297
- for (const result of results) {
298
- const pkValue = this.getPrimaryKeyValue(result)
299
- if (pkValue !== undefined) {
300
- resultsByPrimaryKey.set(pkValue, result)
301
- } else {
302
- resultsWithoutPrimaryKey.push(result)
303
- }
304
- }
305
-
306
- // Prepare after events by matching results back to original inputs
319
+ // Create after events and return results
307
320
  const afterEvents: MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>[] = []
308
- let resultsWithoutPkIndex = 0
309
321
 
310
- // Handle entities with primary keys
311
- for (const entityInfo of entityInfoMap.values()) {
312
- const matchedResult = resultsByPrimaryKey.get(entityInfo.primaryKeyValue)!
322
+ for (let i = 0; i < inputInfos.length; i++) {
323
+ const inputInfo = inputInfos[i]
324
+ const result = results[i]
313
325
 
314
326
  const afterOperation =
315
- entityInfo.operation === ModelMutationAction.BeforeCreate
327
+ inputInfo.operation === ModelMutationAction.BeforeCreate
316
328
  ? ModelMutationAction.AfterCreate
317
329
  : ModelMutationAction.AfterUpdate
318
330
 
319
331
  afterEvents.push(
320
332
  new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
321
333
  this.getDescriptor(afterOperation),
322
- entityInfo.input,
323
- ).setResult(matchedResult),
324
- )
325
- }
326
-
327
- // Handle inputs without primary keys (always creates)
328
- for (const input of inputsWithoutPrimaryKey) {
329
- const matchedResult = resultsWithoutPrimaryKey[resultsWithoutPkIndex++]
330
-
331
- afterEvents.push(
332
- new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
333
- this.getDescriptor(ModelMutationAction.AfterCreate),
334
- input,
335
- ).setResult(matchedResult),
334
+ inputInfo.input,
335
+ ).setResult(result),
336
336
  )
337
337
  }
338
338
 
339
339
  // Emit all after events
340
340
  await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
341
341
 
342
- // Return the results
342
+ // Return normalized results
343
343
  return await Promise.all(results.map((result) => this.normalizeDetail(result)))
344
344
  }
345
345
  }