@declaro/data 2.0.0-beta.96 → 2.0.0-beta.98

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.
@@ -9,6 +9,14 @@ export interface IUpdateOptions extends IActionOptions {
9
9
  }
10
10
  export declare class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelService<TSchema> {
11
11
  constructor(args: IModelServiceArgs<TSchema>);
12
+ /**
13
+ * Normalizes input data before processing. This method can be overridden by subclasses
14
+ * to implement custom input normalization logic (e.g., trimming strings, setting defaults, etc.).
15
+ * By default, this method returns the input unchanged.
16
+ * @param input The input data to normalize.
17
+ * @returns The normalized input data.
18
+ */
19
+ protected normalizeInput(input: InferInput<TSchema>): Promise<InferInput<TSchema>>;
12
20
  /**
13
21
  * Removes a record by its lookup criteria.
14
22
  * @param lookup The lookup criteria to find the record.
@@ -1 +1 @@
1
- {"version":3,"file":"model-service.d.ts","sourceRoot":"","sources":["../../../../src/domain/services/model-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAG7G,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAA;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAG;AACzD,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAG;AAEzD,qBAAa,YAAY,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,oBAAoB,CAAC,OAAO,CAAC;gBAC/E,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;IAI5C;;;;OAIG;IACG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAsBlG;;;;;OAKG;IACG,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAsB7F,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAsB3F,MAAM,CACR,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAC5B,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAC1B,OAAO,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAsBhC;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IA+ClH;;;;;OAKG;IACG,UAAU,CACZ,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,EAC7B,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,GAC1C,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;CA4IrC"}
1
+ {"version":3,"file":"model-service.d.ts","sourceRoot":"","sources":["../../../../src/domain/services/model-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAG7G,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAA;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAG;AACzD,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAG;AAEzD,qBAAa,YAAY,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,oBAAoB,CAAC,OAAO,CAAC;gBAC/E,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;IAI5C;;;;;;OAMG;cACa,cAAc,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAIxF;;;;OAIG;IACG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAsBlG;;;;;OAKG;IACG,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAsB7F,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAyB3F,MAAM,CACR,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAC5B,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAC1B,OAAO,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAyBhC;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAkDlH;;;;;OAKG;IACG,UAAU,CACZ,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,EAC7B,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,GAC1C,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;CA+IrC"}
@@ -7,6 +7,7 @@ export interface IMockMemoryRepositoryArgs<TSchema extends AnyModelSchema> {
7
7
  schema: TSchema;
8
8
  lookup?: (data: InferDetail<TSchema>, lookup: InferLookup<TSchema>) => boolean;
9
9
  filter?: (data: InferSummary<TSchema>, filters: InferFilters<TSchema>) => boolean;
10
+ assign?: (data: InferDetail<TSchema>, input: InferInput<TSchema>) => InferDetail<TSchema>;
10
11
  }
11
12
  export declare class MockMemoryRepository<TSchema extends AnyModelSchema> implements IRepository<TSchema> {
12
13
  protected args: IMockMemoryRepositoryArgs<TSchema>;
@@ -31,6 +32,13 @@ export declare class MockMemoryRepository<TSchema extends AnyModelSchema> implem
31
32
  * @returns Filtered array of items
32
33
  */
33
34
  protected applyFilters(input: InferFilters<TSchema>): InferDetail<TSchema>[];
35
+ /**
36
+ * Assign input data to existing data using the provided assign function or default Object.assign
37
+ * @param existingData - The existing data to merge with
38
+ * @param input - The input data to assign
39
+ * @returns The merged data
40
+ */
41
+ protected assignInput(existingData: InferDetail<TSchema>, input: InferInput<TSchema>): InferDetail<TSchema>;
34
42
  protected generatePrimaryKey(): Promise<string | number>;
35
43
  }
36
44
  //# sourceMappingURL=mock-memory-repository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mock-memory-repository.d.ts","sourceRoot":"","sources":["../../../../../src/test/mock/repositories/mock-memory-repository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mock-memory-repository.d.ts","sourceRoot":"","sources":["../../../../../src/test/mock/repositories/mock-memory-repository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declaro/data",
3
- "version": "2.0.0-beta.96",
3
+ "version": "2.0.0-beta.98",
4
4
  "description": "A data-mapper framework for managing application data across integrated systems.",
5
5
  "main": "dist/node/index.cjs",
6
6
  "module": "dist/node/index.js",
@@ -22,9 +22,9 @@
22
22
  "@declaro/zod": "^2.0.0-beta.51"
23
23
  },
24
24
  "devDependencies": {
25
- "@declaro/auth": "^2.0.0-beta.96",
26
- "@declaro/core": "^2.0.0-beta.96",
27
- "@declaro/zod": "^2.0.0-beta.96",
25
+ "@declaro/auth": "^2.0.0-beta.98",
26
+ "@declaro/core": "^2.0.0-beta.98",
27
+ "@declaro/zod": "^2.0.0-beta.98",
28
28
  "crypto-browserify": "^3.12.1",
29
29
  "typescript": "^5.8.3",
30
30
  "uuid": "^11.1.0",
@@ -43,5 +43,5 @@
43
43
  "require": "./dist/node/index.cjs",
44
44
  "browser": "./dist/browser/index.js"
45
45
  },
46
- "gitHead": "8403f2a99358e7cc3e8b79f3be0972ee48178f5a"
46
+ "gitHead": "5f998cfb45e04482853d7511c04348d668c292da"
47
47
  }
@@ -628,4 +628,249 @@ describe('ModelService', () => {
628
628
  expect(results).toEqual(inputs)
629
629
  })
630
630
  })
631
+
632
+ describe('normalizeInput functionality', () => {
633
+ class TestModelService extends ModelService<typeof mockSchema> {
634
+ public testNormalizeInput(input: any) {
635
+ return this.normalizeInput(input)
636
+ }
637
+
638
+ protected async normalizeInput(input: any) {
639
+ return {
640
+ ...input,
641
+ title: input.title?.trim(),
642
+ author: input.author?.trim(),
643
+ normalizedAt: new Date('2023-01-01'),
644
+ }
645
+ }
646
+ }
647
+
648
+ let testService: TestModelService
649
+
650
+ beforeEach(() => {
651
+ testService = new TestModelService({ repository, emitter, schema: mockSchema, namespace })
652
+ })
653
+
654
+ it('should use default normalizeInput method (no changes) when not overridden', async () => {
655
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
656
+ const normalized = await service['normalizeInput'](input)
657
+
658
+ expect(normalized).toEqual(input)
659
+ expect(normalized).toBe(input) // Should be the exact same reference
660
+ })
661
+
662
+ it('should use custom normalizeInput method for create operation', async () => {
663
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
664
+ const createdItem = await testService.create(input)
665
+
666
+ expect(createdItem.title).toBe('Test Book')
667
+ expect(createdItem.author).toBe('Author Name')
668
+ expect((createdItem as any).normalizedAt).toEqual(new Date('2023-01-01'))
669
+ })
670
+
671
+ it('should use custom normalizeInput method for update operation', async () => {
672
+ const input = { id: 42, title: 'Original Book', author: 'Original Author', publishedDate: new Date() }
673
+ const createdItem = await testService.create(input)
674
+
675
+ const updateInput = { title: ' Updated Book ', author: ' Updated Author ', publishedDate: new Date() }
676
+ const updatedItem = await testService.update({ id: createdItem.id }, updateInput)
677
+
678
+ expect(updatedItem.title).toBe('Updated Book')
679
+ expect(updatedItem.author).toBe('Updated Author')
680
+ expect((updatedItem as any).normalizedAt).toEqual(new Date('2023-01-01'))
681
+ })
682
+
683
+ it('should use custom normalizeInput method for upsert operation', async () => {
684
+ const input = { id: 42, title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
685
+ const upsertedItem = await testService.upsert(input)
686
+
687
+ expect(upsertedItem.title).toBe('Test Book')
688
+ expect(upsertedItem.author).toBe('Author Name')
689
+ expect((upsertedItem as any).normalizedAt).toEqual(new Date('2023-01-01'))
690
+
691
+ // Upsert again with different data
692
+ const updateInput = {
693
+ id: 42,
694
+ title: ' Updated Book ',
695
+ author: ' Updated Author ',
696
+ publishedDate: new Date(),
697
+ }
698
+ const updatedItem = await testService.upsert(updateInput)
699
+
700
+ expect(updatedItem.title).toBe('Updated Book')
701
+ expect(updatedItem.author).toBe('Updated Author')
702
+ expect((updatedItem as any).normalizedAt).toEqual(new Date('2023-01-01'))
703
+ })
704
+
705
+ it('should use custom normalizeInput method for bulkUpsert operation', async () => {
706
+ const inputs = [
707
+ { id: 1, title: ' Book One ', author: ' Author One ', publishedDate: new Date() },
708
+ { id: 2, title: ' Book Two ', author: ' Author Two ', publishedDate: new Date() },
709
+ { title: ' Book Three ', author: ' Author Three ', publishedDate: new Date() }, // No ID - will be created
710
+ ]
711
+
712
+ const results = await testService.bulkUpsert(inputs)
713
+
714
+ expect(results).toHaveLength(3)
715
+ expect(results[0].title).toBe('Book One')
716
+ expect(results[0].author).toBe('Author One')
717
+ expect((results[0] as any).normalizedAt).toEqual(new Date('2023-01-01'))
718
+
719
+ expect(results[1].title).toBe('Book Two')
720
+ expect(results[1].author).toBe('Author Two')
721
+ expect((results[1] as any).normalizedAt).toEqual(new Date('2023-01-01'))
722
+
723
+ expect(results[2].title).toBe('Book Three')
724
+ expect(results[2].author).toBe('Author Three')
725
+ expect((results[2] as any).normalizedAt).toEqual(new Date('2023-01-01'))
726
+ })
727
+
728
+ it('should preserve events order with normalized input in create operation', async () => {
729
+ const input = { title: ' Test Book ', author: ' Author Name ', publishedDate: new Date() }
730
+ await testService.create(input)
731
+
732
+ // Debug: Let's see what was actually called
733
+ const beforeCreateCall = beforeCreateSpy.mock.calls[0][0]
734
+ const afterCreateCall = afterCreateSpy.mock.calls[0][0]
735
+
736
+ expect(beforeCreateCall.meta.input.title).toBe('Test Book')
737
+ expect(beforeCreateCall.meta.input.author).toBe('Author Name')
738
+ expect(beforeCreateCall.meta.input.normalizedAt).toEqual(new Date('2023-01-01'))
739
+
740
+ expect(afterCreateCall.meta.input.title).toBe('Test Book')
741
+ expect(afterCreateCall.meta.input.author).toBe('Author Name')
742
+ expect(afterCreateCall.meta.input.normalizedAt).toEqual(new Date('2023-01-01'))
743
+ })
744
+
745
+ it('should handle complex async normalization logic', async () => {
746
+ class ComplexNormalizationService extends ModelService<typeof mockSchema> {
747
+ protected async normalizeInput(input: any) {
748
+ // Simulate async operations like database lookups, API calls, etc.
749
+ await new Promise((resolve) => setTimeout(resolve, 10))
750
+
751
+ const normalized = { ...input }
752
+
753
+ // Complex normalization logic
754
+ if (normalized.title) {
755
+ normalized.title = normalized.title
756
+ .trim()
757
+ .replace(/\s+/g, ' ')
758
+ .toLowerCase()
759
+ .replace(/\b\w/g, (l: string) => l.toUpperCase()) // Title case
760
+ }
761
+
762
+ if (normalized.author) {
763
+ normalized.author = normalized.author.trim()
764
+ }
765
+
766
+ // Add metadata
767
+ normalized.processedAt = new Date('2023-01-01')
768
+ normalized.version = (normalized.version || 0) + 1
769
+
770
+ return normalized
771
+ }
772
+ }
773
+
774
+ const complexService = new ComplexNormalizationService({
775
+ repository,
776
+ emitter,
777
+ schema: mockSchema,
778
+ namespace,
779
+ })
780
+
781
+ const input = {
782
+ title: ' the great book ',
783
+ author: ' John Doe ',
784
+ publishedDate: new Date(),
785
+ }
786
+
787
+ const result = await complexService.create(input)
788
+
789
+ expect(result.title).toBe('The Great Book')
790
+ expect(result.author).toBe('John Doe')
791
+ expect((result as any).processedAt).toEqual(new Date('2023-01-01'))
792
+ expect((result as any).version).toBe(1)
793
+ })
794
+
795
+ it('should call normalizeInput method exactly once per input during bulkUpsert with Promise.all', async () => {
796
+ const normalizeInputSpy = mock(async (input: any) => ({ ...input, normalized: true }))
797
+
798
+ class SpyService extends ModelService<typeof mockSchema> {
799
+ protected async normalizeInput(input: any) {
800
+ return normalizeInputSpy(input)
801
+ }
802
+ }
803
+
804
+ const spyService = new SpyService({ repository, emitter, schema: mockSchema, namespace })
805
+
806
+ const inputs = [
807
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
808
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
809
+ ]
810
+
811
+ await spyService.bulkUpsert(inputs)
812
+
813
+ expect(normalizeInputSpy).toHaveBeenCalledTimes(2)
814
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(1, inputs[0])
815
+ expect(normalizeInputSpy).toHaveBeenNthCalledWith(2, inputs[1])
816
+ })
817
+
818
+ it('should handle async normalization errors gracefully', async () => {
819
+ class ErrorNormalizationService extends ModelService<typeof mockSchema> {
820
+ protected async normalizeInput(input: any) {
821
+ if (input.title === 'ERROR') {
822
+ throw new Error('Normalization failed')
823
+ }
824
+ return input
825
+ }
826
+ }
827
+
828
+ const errorService = new ErrorNormalizationService({
829
+ repository,
830
+ emitter,
831
+ schema: mockSchema,
832
+ namespace,
833
+ })
834
+
835
+ const input = { title: 'ERROR', author: 'Author Name', publishedDate: new Date() }
836
+
837
+ await expect(errorService.create(input)).rejects.toThrow('Normalization failed')
838
+ })
839
+
840
+ it('should process bulk normalization in parallel for performance', async () => {
841
+ const processingTimes: number[] = []
842
+
843
+ class TimingNormalizationService extends ModelService<typeof mockSchema> {
844
+ protected async normalizeInput(input: any) {
845
+ const start = Date.now()
846
+ // Simulate some async work
847
+ await new Promise((resolve) => setTimeout(resolve, 50))
848
+ processingTimes.push(Date.now() - start)
849
+ return input
850
+ }
851
+ }
852
+
853
+ const timingService = new TimingNormalizationService({
854
+ repository,
855
+ emitter,
856
+ schema: mockSchema,
857
+ namespace,
858
+ })
859
+
860
+ const inputs = [
861
+ { id: 1, title: 'Book One', author: 'Author One', publishedDate: new Date() },
862
+ { id: 2, title: 'Book Two', author: 'Author Two', publishedDate: new Date() },
863
+ { id: 3, title: 'Book Three', author: 'Author Three', publishedDate: new Date() },
864
+ ]
865
+
866
+ const start = Date.now()
867
+ await timingService.bulkUpsert(inputs)
868
+ const totalTime = Date.now() - start
869
+
870
+ // With Promise.all, total time should be closer to single operation time rather than sum of all
871
+ // Allow some variance for test stability
872
+ expect(totalTime).toBeLessThan(150) // Much less than 3 * 50ms = 150ms
873
+ expect(processingTimes).toHaveLength(3)
874
+ })
875
+ })
631
876
  })
@@ -14,6 +14,17 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
14
14
  super(args)
15
15
  }
16
16
 
17
+ /**
18
+ * Normalizes input data before processing. This method can be overridden by subclasses
19
+ * to implement custom input normalization logic (e.g., trimming strings, setting defaults, etc.).
20
+ * By default, this method returns the input unchanged.
21
+ * @param input The input data to normalize.
22
+ * @returns The normalized input data.
23
+ */
24
+ protected async normalizeInput(input: InferInput<TSchema>): Promise<InferInput<TSchema>> {
25
+ return input
26
+ }
27
+
17
28
  /**
18
29
  * Removes a record by its lookup criteria.
19
30
  * @param lookup The lookup criteria to find the record.
@@ -70,20 +81,23 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
70
81
  }
71
82
 
72
83
  async create(input: InferInput<TSchema>, options?: ICreateOptions): Promise<InferDetail<TSchema>> {
84
+ // Normalize the input data
85
+ const normalizedInput = await this.normalizeInput(input)
86
+
73
87
  // Emit the before create event
74
88
  const beforeCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
75
89
  this.getDescriptor(ModelMutationAction.BeforeCreate),
76
- input,
90
+ normalizedInput,
77
91
  )
78
92
  await this.emitter.emitAsync(beforeCreateEvent)
79
93
 
80
94
  // Perform the creation
81
- const result = await this.repository.create(input, options)
95
+ const result = await this.repository.create(normalizedInput, options)
82
96
 
83
97
  // Emit the after create event
84
98
  const afterCreateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
85
99
  this.getDescriptor(ModelMutationAction.AfterCreate),
86
- input,
100
+ normalizedInput,
87
101
  ).setResult(result)
88
102
  await this.emitter.emitAsync(afterCreateEvent)
89
103
 
@@ -96,20 +110,23 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
96
110
  input: InferInput<TSchema>,
97
111
  options?: IUpdateOptions,
98
112
  ): Promise<InferDetail<TSchema>> {
113
+ // Normalize the input data
114
+ const normalizedInput = await this.normalizeInput(input)
115
+
99
116
  // Emit the before update event
100
117
  const beforeUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
101
118
  this.getDescriptor(ModelMutationAction.BeforeUpdate),
102
- input,
119
+ normalizedInput,
103
120
  )
104
121
  await this.emitter.emitAsync(beforeUpdateEvent)
105
122
 
106
123
  // Perform the update
107
- const result = await this.repository.update(lookup, input, options)
124
+ const result = await this.repository.update(lookup, normalizedInput, options)
108
125
 
109
126
  // Emit the after update event
110
127
  const afterUpdateEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
111
128
  this.getDescriptor(ModelMutationAction.AfterUpdate),
112
- input,
129
+ normalizedInput,
113
130
  ).setResult(result)
114
131
  await this.emitter.emitAsync(afterUpdateEvent)
115
132
 
@@ -124,7 +141,10 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
124
141
  * @returns The upserted record.
125
142
  */
126
143
  async upsert(input: InferInput<TSchema>, options?: ICreateOptions | IUpdateOptions): Promise<InferDetail<TSchema>> {
127
- const primaryKeyValue = this.getPrimaryKeyValue(input)
144
+ // Normalize the input data
145
+ const normalizedInput = await this.normalizeInput(input)
146
+
147
+ const primaryKeyValue = this.getPrimaryKeyValue(normalizedInput)
128
148
 
129
149
  let beforeOperation: ModelMutationAction
130
150
  let afterOperation: ModelMutationAction
@@ -152,17 +172,17 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
152
172
  // Emit the before upsert event
153
173
  const beforeUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
154
174
  this.getDescriptor(beforeOperation),
155
- input,
175
+ normalizedInput,
156
176
  )
157
177
  await this.emitter.emitAsync(beforeUpsertEvent)
158
178
 
159
179
  // Perform the upsert operation
160
- const result = await this.repository.upsert(input, options)
180
+ const result = await this.repository.upsert(normalizedInput, options)
161
181
 
162
182
  // Emit the after upsert event
163
183
  const afterUpsertEvent = new MutationEvent<InferDetail<TSchema>, InferInput<TSchema>>(
164
184
  this.getDescriptor(afterOperation),
165
- input,
185
+ normalizedInput,
166
186
  ).setResult(result)
167
187
  await this.emitter.emitAsync(afterUpsertEvent)
168
188
 
@@ -184,6 +204,9 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
184
204
  return []
185
205
  }
186
206
 
207
+ // Normalize all input data in parallel using Promise.all
208
+ const normalizedInputs = await Promise.all(inputs.map((input) => this.normalizeInput(input)))
209
+
187
210
  // Build a map of primary key to input and lookup info
188
211
  type EntityInfo = {
189
212
  input: InferInput<TSchema>
@@ -196,8 +219,8 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
196
219
  const entityInfoMap = new Map<string | number, EntityInfo>()
197
220
  const inputsWithoutPrimaryKey: InferInput<TSchema>[] = []
198
221
 
199
- // Process each input and organize by primary key
200
- for (const input of inputs) {
222
+ // Process each normalized input and organize by primary key
223
+ for (const input of normalizedInputs) {
201
224
  const primaryKeyValue = this.getPrimaryKeyValue(input)
202
225
 
203
226
  if (primaryKeyValue !== undefined) {
@@ -264,8 +287,8 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
264
287
  // Emit all before events
265
288
  await Promise.all(beforeEvents.map((event) => this.emitter.emitAsync(event)))
266
289
 
267
- // Perform the bulk upsert operation
268
- const results = await this.repository.bulkUpsert(inputs, options)
290
+ // Perform the bulk upsert operation with normalized inputs
291
+ const results = await this.repository.bulkUpsert(normalizedInputs, options)
269
292
 
270
293
  // Create a map of result primary keys to results for matching
271
294
  const resultsByPrimaryKey = new Map<string | number, InferDetail<TSchema>>()