@declaro/data 2.0.0-beta.116 → 2.0.0-beta.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.js +14 -14
- package/dist/browser/index.js.map +4 -4
- package/dist/node/index.cjs +63 -52
- package/dist/node/index.cjs.map +4 -4
- package/dist/node/index.js +63 -52
- package/dist/node/index.js.map +4 -4
- package/dist/ts/domain/events/event-types.d.ts +4 -0
- package/dist/ts/domain/events/event-types.d.ts.map +1 -1
- package/dist/ts/domain/services/model-service.d.ts +6 -2
- package/dist/ts/domain/services/model-service.d.ts.map +1 -1
- 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/package.json +5 -5
- package/src/domain/events/event-types.ts +4 -0
- package/src/domain/services/model-service.normalization.test.ts +704 -0
- package/src/domain/services/model-service.test.ts +1 -428
- package/src/domain/services/model-service.ts +91 -91
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
primaryKeyValue
|
|
232
|
+
index: number
|
|
233
|
+
primaryKeyValue?: string | number
|
|
215
234
|
existingEntity?: InferDetail<TSchema>
|
|
216
235
|
operation?: ModelMutationAction
|
|
217
236
|
}
|
|
218
237
|
|
|
219
|
-
const
|
|
220
|
-
const
|
|
238
|
+
const inputInfos: InputInfo[] = []
|
|
239
|
+
const uniqueLookups = new Map<string | number, InferLookup<TSchema>>()
|
|
221
240
|
|
|
222
|
-
// Process each
|
|
223
|
-
for (
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
entityInfo.existingEntity = entity
|
|
269
|
+
if (pkValue !== undefined) {
|
|
270
|
+
existingEntitiesMap.set(pkValue, entity)
|
|
253
271
|
}
|
|
254
272
|
}
|
|
255
273
|
})
|
|
256
274
|
}
|
|
257
275
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
).setResult(
|
|
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
|
|
342
|
+
// Return normalized results
|
|
343
343
|
return await Promise.all(results.map((result) => this.normalizeDetail(result)))
|
|
344
344
|
}
|
|
345
345
|
}
|