@declaro/data 2.0.0-beta.110 → 2.0.0-beta.111
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 +6 -6
- package/dist/browser/index.js.map +4 -4
- package/dist/node/index.cjs +37 -10
- package/dist/node/index.cjs.map +4 -4
- package/dist/node/index.js +37 -10
- package/dist/node/index.js.map +4 -4
- package/dist/ts/domain/services/read-only-model-service.d.ts +17 -0
- package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/domain/services/model-service.test.ts +207 -62
- package/src/domain/services/model-service.ts +6 -6
- package/src/domain/services/read-only-model-service.test.ts +103 -3
- package/src/domain/services/read-only-model-service.ts +48 -4
- package/dist/ts/test/domain/services/model-service.test.d.ts +0 -1
- package/dist/ts/test/domain/services/model-service.test.d.ts.map +0 -1
- package/src/test/domain/services/model-service.test.ts +0 -0
|
@@ -9,6 +9,23 @@ export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionO
|
|
|
9
9
|
sort?: InferSort<TSchema>;
|
|
10
10
|
}
|
|
11
11
|
export declare class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
|
|
12
|
+
/**
|
|
13
|
+
* Normalize the detail data to match the expected schema.
|
|
14
|
+
* WARNING: This method is called once per detail in load operations.
|
|
15
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `loadMany` instead.
|
|
16
|
+
* @param detail The detail data to normalize.
|
|
17
|
+
* @returns The normalized detail data.
|
|
18
|
+
*/
|
|
19
|
+
normalizeDetail(detail: InferDetail<TSchema>): Promise<InferDetail<TSchema>>;
|
|
20
|
+
/**
|
|
21
|
+
* Normalize the summary data to match the expected schema.
|
|
22
|
+
* WARNING: This method is called once per summary in search results, often in parallel.
|
|
23
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `search` instead.
|
|
24
|
+
*
|
|
25
|
+
* @param summary The summary data to normalize.
|
|
26
|
+
* @returns The normalized summary data.
|
|
27
|
+
*/
|
|
28
|
+
normalizeSummary(summary: InferDetail<TSchema>): Promise<InferDetail<TSchema>>;
|
|
12
29
|
/**
|
|
13
30
|
* Load a single record by its lookup criteria.
|
|
14
31
|
* @param lookup The lookup criteria to find the record.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-only-model-service.d.ts","sourceRoot":"","sources":["../../../../src/domain/services/read-only-model-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"read-only-model-service.d.ts","sourceRoot":"","sources":["../../../../src/domain/services/read-only-model-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EACR,WAAW,EACX,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,SAAS,EACZ,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,MAAM,WAAW,YAAa,SAAQ,cAAc;CAAG;AACvD,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,cAAc;IAClF,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,IAAI,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;CAC5B;AAED,qBAAa,oBAAoB,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,gBAAgB,CAAC,OAAO,CAAC;IAC/F;;;;;;OAMG;IACG,eAAe,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAalF;;;;;;;OAOG;IACG,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAapF;;;;;OAKG;IACG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAqB/F;;;;;OAKG;IACG,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAqBxG;;;;;OAKG;IACG,MAAM,CACR,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAyBvC;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAqBlG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@declaro/data",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.111",
|
|
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.
|
|
26
|
-
"@declaro/core": "^2.0.0-beta.
|
|
27
|
-
"@declaro/zod": "^2.0.0-beta.
|
|
25
|
+
"@declaro/auth": "^2.0.0-beta.111",
|
|
26
|
+
"@declaro/core": "^2.0.0-beta.111",
|
|
27
|
+
"@declaro/zod": "^2.0.0-beta.111",
|
|
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": "
|
|
46
|
+
"gitHead": "a4506956d649e65c945a96482df1156be7510d85"
|
|
47
47
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'bun:test'
|
|
2
2
|
import { ModelService } from './model-service'
|
|
3
3
|
import { MockMemoryRepository } from '../../test/mock/repositories/mock-memory-repository'
|
|
4
|
-
import { MockBookSchema } from '../../test/mock/models/mock-book-models'
|
|
4
|
+
import { MockBookSchema, type MockBookInput } from '../../test/mock/models/mock-book-models'
|
|
5
5
|
import { EventManager } from '@declaro/core'
|
|
6
6
|
import { mock } from 'bun:test'
|
|
7
7
|
|
|
@@ -635,12 +635,12 @@ describe('ModelService', () => {
|
|
|
635
635
|
return this.normalizeInput(input)
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
-
protected async normalizeInput(input:
|
|
638
|
+
protected async normalizeInput(input: MockBookInput): Promise<MockBookInput> {
|
|
639
639
|
return {
|
|
640
640
|
...input,
|
|
641
641
|
title: input.title?.trim(),
|
|
642
642
|
author: input.author?.trim(),
|
|
643
|
-
|
|
643
|
+
publishedDate: new Date('2023-01-01'),
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
}
|
|
@@ -665,7 +665,7 @@ describe('ModelService', () => {
|
|
|
665
665
|
|
|
666
666
|
expect(createdItem.title).toBe('Test Book')
|
|
667
667
|
expect(createdItem.author).toBe('Author Name')
|
|
668
|
-
expect(
|
|
668
|
+
expect(createdItem.publishedDate).toEqual(new Date('2023-01-01'))
|
|
669
669
|
})
|
|
670
670
|
|
|
671
671
|
it('should use custom normalizeInput method for update operation', async () => {
|
|
@@ -677,7 +677,7 @@ describe('ModelService', () => {
|
|
|
677
677
|
|
|
678
678
|
expect(updatedItem.title).toBe('Updated Book')
|
|
679
679
|
expect(updatedItem.author).toBe('Updated Author')
|
|
680
|
-
expect(
|
|
680
|
+
expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
|
|
681
681
|
})
|
|
682
682
|
|
|
683
683
|
it('should use custom normalizeInput method for upsert operation', async () => {
|
|
@@ -686,7 +686,7 @@ describe('ModelService', () => {
|
|
|
686
686
|
|
|
687
687
|
expect(upsertedItem.title).toBe('Test Book')
|
|
688
688
|
expect(upsertedItem.author).toBe('Author Name')
|
|
689
|
-
expect(
|
|
689
|
+
expect(upsertedItem.publishedDate).toEqual(new Date('2023-01-01'))
|
|
690
690
|
|
|
691
691
|
// Upsert again with different data
|
|
692
692
|
const updateInput = {
|
|
@@ -699,7 +699,7 @@ describe('ModelService', () => {
|
|
|
699
699
|
|
|
700
700
|
expect(updatedItem.title).toBe('Updated Book')
|
|
701
701
|
expect(updatedItem.author).toBe('Updated Author')
|
|
702
|
-
expect(
|
|
702
|
+
expect(updatedItem.publishedDate).toEqual(new Date('2023-01-01'))
|
|
703
703
|
})
|
|
704
704
|
|
|
705
705
|
it('should use custom normalizeInput method for bulkUpsert operation', async () => {
|
|
@@ -714,15 +714,15 @@ describe('ModelService', () => {
|
|
|
714
714
|
expect(results).toHaveLength(3)
|
|
715
715
|
expect(results[0].title).toBe('Book One')
|
|
716
716
|
expect(results[0].author).toBe('Author One')
|
|
717
|
-
expect(
|
|
717
|
+
expect(results[0].publishedDate).toEqual(new Date('2023-01-01'))
|
|
718
718
|
|
|
719
719
|
expect(results[1].title).toBe('Book Two')
|
|
720
720
|
expect(results[1].author).toBe('Author Two')
|
|
721
|
-
expect(
|
|
721
|
+
expect(results[1].publishedDate).toEqual(new Date('2023-01-01'))
|
|
722
722
|
|
|
723
723
|
expect(results[2].title).toBe('Book Three')
|
|
724
724
|
expect(results[2].author).toBe('Author Three')
|
|
725
|
-
expect(
|
|
725
|
+
expect(results[2].publishedDate).toEqual(new Date('2023-01-01'))
|
|
726
726
|
})
|
|
727
727
|
|
|
728
728
|
it('should preserve events order with normalized input in create operation', async () => {
|
|
@@ -735,61 +735,11 @@ describe('ModelService', () => {
|
|
|
735
735
|
|
|
736
736
|
expect(beforeCreateCall.meta.input.title).toBe('Test Book')
|
|
737
737
|
expect(beforeCreateCall.meta.input.author).toBe('Author Name')
|
|
738
|
-
expect(beforeCreateCall.meta.input.
|
|
738
|
+
expect(beforeCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
|
|
739
739
|
|
|
740
740
|
expect(afterCreateCall.meta.input.title).toBe('Test Book')
|
|
741
741
|
expect(afterCreateCall.meta.input.author).toBe('Author Name')
|
|
742
|
-
expect(afterCreateCall.meta.input.
|
|
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)
|
|
742
|
+
expect(afterCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
|
|
793
743
|
})
|
|
794
744
|
|
|
795
745
|
it('should call normalizeInput method exactly once per input during bulkUpsert with Promise.all', async () => {
|
|
@@ -873,4 +823,199 @@ describe('ModelService', () => {
|
|
|
873
823
|
expect(processingTimes).toHaveLength(3)
|
|
874
824
|
})
|
|
875
825
|
})
|
|
826
|
+
|
|
827
|
+
describe('Response Normalization', () => {
|
|
828
|
+
class TestRepository extends MockMemoryRepository<typeof mockSchema> {
|
|
829
|
+
constructor() {
|
|
830
|
+
super({
|
|
831
|
+
schema: mockSchema,
|
|
832
|
+
})
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async create(input: MockBookInput): Promise<any> {
|
|
836
|
+
const record = await super.create(input)
|
|
837
|
+
// Return with publishedDate as string to test normalization
|
|
838
|
+
record.publishedDate = '2024-01-01' as any
|
|
839
|
+
return record
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async update(lookup: any, input: MockBookInput): Promise<any> {
|
|
843
|
+
const record = await super.update(lookup, input)
|
|
844
|
+
// Return with publishedDate as string to test normalization
|
|
845
|
+
record.publishedDate = '2024-01-01' as any
|
|
846
|
+
return record
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async upsert(input: MockBookInput): Promise<any> {
|
|
850
|
+
const record = await super.upsert(input)
|
|
851
|
+
// Return with publishedDate as string to test normalization
|
|
852
|
+
record.publishedDate = '2024-01-01' as any
|
|
853
|
+
return record
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async bulkUpsert(inputs: MockBookInput[]): Promise<any[]> {
|
|
857
|
+
const records = await super.bulkUpsert(inputs)
|
|
858
|
+
// Return with publishedDate as string to test normalization
|
|
859
|
+
for (const record of records) {
|
|
860
|
+
record.publishedDate = '2024-01-01' as any
|
|
861
|
+
}
|
|
862
|
+
return records
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async remove(lookup: any): Promise<any> {
|
|
866
|
+
const record = await super.remove(lookup)
|
|
867
|
+
// Return with publishedDate as string to test normalization
|
|
868
|
+
if (record) {
|
|
869
|
+
record.publishedDate = '2024-01-01' as any
|
|
870
|
+
}
|
|
871
|
+
return record
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async restore(lookup: any): Promise<any> {
|
|
875
|
+
const record = await super.restore(lookup)
|
|
876
|
+
// Return with publishedDate as string to test normalization
|
|
877
|
+
if (record) {
|
|
878
|
+
record.publishedDate = '2024-01-01' as any
|
|
879
|
+
}
|
|
880
|
+
return record
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
class TestService extends ModelService<typeof mockSchema> {}
|
|
885
|
+
|
|
886
|
+
let testRepository: TestRepository
|
|
887
|
+
let testService: TestService
|
|
888
|
+
|
|
889
|
+
beforeEach(() => {
|
|
890
|
+
testRepository = new TestRepository()
|
|
891
|
+
emitter = new EventManager()
|
|
892
|
+
|
|
893
|
+
testService = new TestService({
|
|
894
|
+
repository: testRepository,
|
|
895
|
+
emitter,
|
|
896
|
+
schema: mockSchema,
|
|
897
|
+
namespace,
|
|
898
|
+
})
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
it('should normalize details in the create response', async () => {
|
|
902
|
+
const input = { id: 200, title: 'Create Test', author: 'Creator', publishedDate: new Date() }
|
|
903
|
+
const record = await testService.create(input)
|
|
904
|
+
|
|
905
|
+
const expectedDate = new Date('2024-01-01')
|
|
906
|
+
const actualDate = record.publishedDate
|
|
907
|
+
|
|
908
|
+
expect(actualDate).toEqual(expectedDate)
|
|
909
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('should normalize details in the update response', async () => {
|
|
913
|
+
const input = { id: 201, title: 'Update Test', author: 'Updater', publishedDate: new Date() }
|
|
914
|
+
await testRepository.create(input)
|
|
915
|
+
|
|
916
|
+
const updatedInput = { title: 'Updated Test', author: 'Updated Author', publishedDate: new Date() }
|
|
917
|
+
const record = await testService.update({ id: 201 }, updatedInput)
|
|
918
|
+
|
|
919
|
+
const expectedDate = new Date('2024-01-01')
|
|
920
|
+
const actualDate = record.publishedDate
|
|
921
|
+
|
|
922
|
+
expect(actualDate).toEqual(expectedDate)
|
|
923
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
it('should normalize details in the upsert response when creating', async () => {
|
|
927
|
+
const input = { id: 202, title: 'Upsert Create Test', author: 'Upserter', publishedDate: new Date() }
|
|
928
|
+
const record = await testService.upsert(input)
|
|
929
|
+
|
|
930
|
+
const expectedDate = new Date('2024-01-01')
|
|
931
|
+
const actualDate = record.publishedDate
|
|
932
|
+
|
|
933
|
+
expect(actualDate).toEqual(expectedDate)
|
|
934
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
it('should normalize details in the upsert response when updating', async () => {
|
|
938
|
+
const input = { id: 203, title: 'Upsert Update Test', author: 'Upserter', publishedDate: new Date() }
|
|
939
|
+
await testRepository.create(input)
|
|
940
|
+
|
|
941
|
+
const updatedInput = {
|
|
942
|
+
id: 203,
|
|
943
|
+
title: 'Updated Upsert Test',
|
|
944
|
+
author: 'Updated Upserter',
|
|
945
|
+
publishedDate: new Date(),
|
|
946
|
+
}
|
|
947
|
+
const record = await testService.upsert(updatedInput)
|
|
948
|
+
|
|
949
|
+
const expectedDate = new Date('2024-01-01')
|
|
950
|
+
const actualDate = record.publishedDate
|
|
951
|
+
|
|
952
|
+
expect(actualDate).toEqual(expectedDate)
|
|
953
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
it('should normalize details in the bulkUpsert response', async () => {
|
|
957
|
+
const input1 = { id: 204, title: 'Bulk Test 1', author: 'Bulk Author 1', publishedDate: new Date() }
|
|
958
|
+
const input2 = { id: 205, title: 'Bulk Test 2', author: 'Bulk Author 2', publishedDate: new Date() }
|
|
959
|
+
|
|
960
|
+
const records = await testService.bulkUpsert([input1, input2])
|
|
961
|
+
|
|
962
|
+
const expectedDate = new Date('2024-01-01')
|
|
963
|
+
|
|
964
|
+
for (const record of records) {
|
|
965
|
+
const actualDate = record.publishedDate
|
|
966
|
+
expect(actualDate).toEqual(expectedDate)
|
|
967
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
968
|
+
}
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('should normalize details in the bulkUpsert response with mixed create and update', async () => {
|
|
972
|
+
const existingInput = { id: 206, title: 'Existing', author: 'Existing Author', publishedDate: new Date() }
|
|
973
|
+
await testRepository.create(existingInput)
|
|
974
|
+
|
|
975
|
+
const updateInput = {
|
|
976
|
+
id: 206,
|
|
977
|
+
title: 'Updated Existing',
|
|
978
|
+
author: 'Updated Author',
|
|
979
|
+
publishedDate: new Date(),
|
|
980
|
+
}
|
|
981
|
+
const createInput = { id: 207, title: 'New Record', author: 'New Author', publishedDate: new Date() }
|
|
982
|
+
|
|
983
|
+
const records = await testService.bulkUpsert([updateInput, createInput])
|
|
984
|
+
|
|
985
|
+
const expectedDate = new Date('2024-01-01')
|
|
986
|
+
|
|
987
|
+
for (const record of records) {
|
|
988
|
+
const actualDate = record.publishedDate
|
|
989
|
+
expect(actualDate).toEqual(expectedDate)
|
|
990
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
991
|
+
}
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
it('should normalize summaries in the remove response', async () => {
|
|
995
|
+
const input = { id: 208, title: 'Remove Test', author: 'Remover', publishedDate: new Date() }
|
|
996
|
+
await testRepository.create(input)
|
|
997
|
+
|
|
998
|
+
const record = await testService.remove({ id: 208 })
|
|
999
|
+
|
|
1000
|
+
const expectedDate = new Date('2024-01-01')
|
|
1001
|
+
const actualDate = record.publishedDate
|
|
1002
|
+
|
|
1003
|
+
expect(actualDate).toEqual(expectedDate)
|
|
1004
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
it('should normalize summaries in the restore response', async () => {
|
|
1008
|
+
const input = { id: 209, title: 'Restore Test', author: 'Restorer', publishedDate: new Date() }
|
|
1009
|
+
await testRepository.create(input)
|
|
1010
|
+
await testRepository.remove({ id: 209 })
|
|
1011
|
+
|
|
1012
|
+
const record = await testService.restore({ id: 209 })
|
|
1013
|
+
|
|
1014
|
+
const expectedDate = new Date('2024-01-01')
|
|
1015
|
+
const actualDate = record.publishedDate
|
|
1016
|
+
|
|
1017
|
+
expect(actualDate).toEqual(expectedDate)
|
|
1018
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
1019
|
+
})
|
|
1020
|
+
})
|
|
876
1021
|
})
|
|
@@ -49,7 +49,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
49
49
|
await this.emitter.emitAsync(afterRemoveEvent)
|
|
50
50
|
|
|
51
51
|
// Return the results of the removal
|
|
52
|
-
return result
|
|
52
|
+
return await this.normalizeSummary(result)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -77,7 +77,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
77
77
|
await this.emitter.emitAsync(afterRestoreEvent)
|
|
78
78
|
|
|
79
79
|
// Return the results of the restore operation
|
|
80
|
-
return result
|
|
80
|
+
return await this.normalizeSummary(result)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async create(input: InferInput<TSchema>, options?: ICreateOptions): Promise<InferDetail<TSchema>> {
|
|
@@ -102,7 +102,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
102
102
|
await this.emitter.emitAsync(afterCreateEvent)
|
|
103
103
|
|
|
104
104
|
// Return the results of the creation
|
|
105
|
-
return result
|
|
105
|
+
return await this.normalizeDetail(result)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async update(
|
|
@@ -131,7 +131,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
131
131
|
await this.emitter.emitAsync(afterUpdateEvent)
|
|
132
132
|
|
|
133
133
|
// Return the results of the update
|
|
134
|
-
return result
|
|
134
|
+
return await this.normalizeDetail(result)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
@@ -187,7 +187,7 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
187
187
|
await this.emitter.emitAsync(afterUpsertEvent)
|
|
188
188
|
|
|
189
189
|
// Return the results of the upsert operation
|
|
190
|
-
return result
|
|
190
|
+
return await this.normalizeDetail(result)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
@@ -340,6 +340,6 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
|
|
|
340
340
|
await Promise.all(afterEvents.map((event) => this.emitter.emitAsync(event)))
|
|
341
341
|
|
|
342
342
|
// Return the results
|
|
343
|
-
return results
|
|
343
|
+
return await Promise.all(results.map((result) => this.normalizeDetail(result)))
|
|
344
344
|
}
|
|
345
345
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, spyOn, mock } from 'bun:test'
|
|
2
|
-
import { ReadOnlyModelService } from './read-only-model-service'
|
|
1
|
+
import { describe, it, expect, beforeEach, spyOn, mock, beforeAll } from 'bun:test'
|
|
2
|
+
import { ReadOnlyModelService, type ILoadOptions } from './read-only-model-service'
|
|
3
3
|
import { MockMemoryRepository } from '../../test/mock/repositories/mock-memory-repository'
|
|
4
|
-
import { MockBookSchema } from '../../test/mock/models/mock-book-models'
|
|
4
|
+
import { MockBookSchema, type MockBookDetail, type MockBookLookup } from '../../test/mock/models/mock-book-models'
|
|
5
5
|
import { EventManager } from '@declaro/core'
|
|
6
6
|
import type { QueryEvent } from '../events/query-event'
|
|
7
7
|
import type { InferDetail, InferFilters, InferLookup, InferSearchResults } from '../../shared/utils/schema-inference'
|
|
@@ -293,4 +293,104 @@ describe('ReadOnlyModelService', () => {
|
|
|
293
293
|
expect(afterSearchSpy).toHaveBeenCalledTimes(1)
|
|
294
294
|
expect(afterSearchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'books::book.afterSearch' }))
|
|
295
295
|
})
|
|
296
|
+
|
|
297
|
+
describe('Response Normalization', () => {
|
|
298
|
+
class TestRepository extends MockMemoryRepository<typeof mockSchema> {
|
|
299
|
+
constructor() {
|
|
300
|
+
super({
|
|
301
|
+
schema: mockSchema,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async load(input: MockBookLookup): Promise<MockBookDetail | null> {
|
|
306
|
+
const record = await super.load(input)
|
|
307
|
+
|
|
308
|
+
if (record) {
|
|
309
|
+
record.publishedDate = '2024-01-01' as any
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return record
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async loadMany(inputs: MockBookLookup[]): Promise<MockBookDetail[]> {
|
|
316
|
+
const records = await super.loadMany(inputs)
|
|
317
|
+
|
|
318
|
+
for (const record of records) {
|
|
319
|
+
record.publishedDate = '2024-01-01' as any
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return records
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async search(
|
|
326
|
+
filters: InferFilters<typeof mockSchema>,
|
|
327
|
+
options?: ILoadOptions,
|
|
328
|
+
): Promise<InferSearchResults<typeof mockSchema>> {
|
|
329
|
+
const results = await super.search(filters, options)
|
|
330
|
+
|
|
331
|
+
for (const record of results.results) {
|
|
332
|
+
record.publishedDate = '2024-01-01' as any
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return results
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
class TestService extends ReadOnlyModelService<typeof mockSchema> {}
|
|
339
|
+
|
|
340
|
+
let testService: TestService
|
|
341
|
+
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
repository = new TestRepository()
|
|
344
|
+
emitter = new EventManager()
|
|
345
|
+
|
|
346
|
+
testService = new TestService({ repository, emitter, schema: mockSchema, namespace })
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should normalize details in the load response', async () => {
|
|
350
|
+
const input = { id: 100, title: 'Normalization Test', author: 'Normalizer', publishedDate: new Date() }
|
|
351
|
+
await repository.create(input)
|
|
352
|
+
|
|
353
|
+
const record = await testService.load({ id: 100 })
|
|
354
|
+
|
|
355
|
+
const expectedDate = new Date('2024-01-01')
|
|
356
|
+
const actualDate = record.publishedDate
|
|
357
|
+
|
|
358
|
+
expect(actualDate).toEqual(expectedDate)
|
|
359
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should normalize details in the loadMany response', async () => {
|
|
363
|
+
const input1 = { id: 101, title: 'Normalization Test 1', author: 'Normalizer 1', publishedDate: new Date() }
|
|
364
|
+
const input2 = { id: 102, title: 'Normalization Test 2', author: 'Normalizer 2', publishedDate: new Date() }
|
|
365
|
+
await repository.create(input1)
|
|
366
|
+
await repository.create(input2)
|
|
367
|
+
|
|
368
|
+
const records = await testService.loadMany([{ id: 101 }, { id: 102 }])
|
|
369
|
+
|
|
370
|
+
const expectedDate = new Date('2024-01-01')
|
|
371
|
+
|
|
372
|
+
for (const record of records) {
|
|
373
|
+
const actualDate = record.publishedDate
|
|
374
|
+
expect(actualDate).toEqual(expectedDate)
|
|
375
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should normalize details in the search response', async () => {
|
|
380
|
+
const input1 = { id: 103, title: 'Normalization Test 3', author: 'Normalizer 3', publishedDate: new Date() }
|
|
381
|
+
const input2 = { id: 104, title: 'Normalization Test 4', author: 'Normalizer 4', publishedDate: new Date() }
|
|
382
|
+
await repository.create(input1)
|
|
383
|
+
await repository.create(input2)
|
|
384
|
+
|
|
385
|
+
const results = await testService.search({ text: 'Normalization' })
|
|
386
|
+
|
|
387
|
+
const expectedDate = new Date('2024-01-01')
|
|
388
|
+
|
|
389
|
+
for (const record of results.results) {
|
|
390
|
+
const actualDate = record.publishedDate
|
|
391
|
+
expect(actualDate).toEqual(expectedDate)
|
|
392
|
+
expect(actualDate).toBeInstanceOf(Date)
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
})
|
|
296
396
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyModelSchema } from '@declaro/core'
|
|
1
|
+
import type { AnyModelSchema, Model } from '@declaro/core'
|
|
2
2
|
import type {
|
|
3
3
|
InferDetail,
|
|
4
4
|
InferFilters,
|
|
@@ -19,6 +19,47 @@ export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionO
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
|
|
22
|
+
/**
|
|
23
|
+
* Normalize the detail data to match the expected schema.
|
|
24
|
+
* WARNING: This method is called once per detail in load operations.
|
|
25
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `loadMany` instead.
|
|
26
|
+
* @param detail The detail data to normalize.
|
|
27
|
+
* @returns The normalized detail data.
|
|
28
|
+
*/
|
|
29
|
+
async normalizeDetail(detail: InferDetail<TSchema>): Promise<InferDetail<TSchema>> {
|
|
30
|
+
const detailModel = this.schema.definition.detail as Model<any, any>
|
|
31
|
+
if (detailModel) {
|
|
32
|
+
const validation = await detailModel.validate(detail, { strict: false })
|
|
33
|
+
if (validation.issues) {
|
|
34
|
+
console.warn(`${detailModel.labels.singularLabel} shape did not match the expected schema`)
|
|
35
|
+
} else {
|
|
36
|
+
return validation.value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return detail
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize the summary data to match the expected schema.
|
|
44
|
+
* WARNING: This method is called once per summary in search results, often in parallel.
|
|
45
|
+
* Any intensive operations or queries should be avoided here, and done via bulk operations in the respective methods such as `search` instead.
|
|
46
|
+
*
|
|
47
|
+
* @param summary The summary data to normalize.
|
|
48
|
+
* @returns The normalized summary data.
|
|
49
|
+
*/
|
|
50
|
+
async normalizeSummary(summary: InferDetail<TSchema>): Promise<InferDetail<TSchema>> {
|
|
51
|
+
const summaryModel = this.schema.definition.summary as Model<any, any>
|
|
52
|
+
if (summaryModel) {
|
|
53
|
+
const validation = await summaryModel.validate(summary, { strict: false })
|
|
54
|
+
if (validation.issues) {
|
|
55
|
+
console.warn(`${summaryModel.labels.singularLabel} shape did not match the expected schema`)
|
|
56
|
+
} else {
|
|
57
|
+
return validation.value
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return summary
|
|
61
|
+
}
|
|
62
|
+
|
|
22
63
|
/**
|
|
23
64
|
* Load a single record by its lookup criteria.
|
|
24
65
|
* @param lookup The lookup criteria to find the record.
|
|
@@ -43,7 +84,7 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
|
|
|
43
84
|
).setResult(details)
|
|
44
85
|
await this.emitter.emitAsync(afterLoadEvent)
|
|
45
86
|
|
|
46
|
-
return details
|
|
87
|
+
return await this.normalizeDetail(details)
|
|
47
88
|
}
|
|
48
89
|
|
|
49
90
|
/**
|
|
@@ -70,7 +111,7 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
|
|
|
70
111
|
).setResult(details)
|
|
71
112
|
await this.emitter.emitAsync(afterLoadManyEvent)
|
|
72
113
|
|
|
73
|
-
return details
|
|
114
|
+
return await Promise.all(details.map((detail) => this.normalizeDetail(detail)))
|
|
74
115
|
}
|
|
75
116
|
|
|
76
117
|
/**
|
|
@@ -101,7 +142,10 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
|
|
|
101
142
|
await this.emitter.emitAsync(afterSearchEvent)
|
|
102
143
|
|
|
103
144
|
// Return the search results
|
|
104
|
-
return
|
|
145
|
+
return {
|
|
146
|
+
...results,
|
|
147
|
+
results: await Promise.all(results.results.map((detail) => this.normalizeSummary(detail))),
|
|
148
|
+
}
|
|
105
149
|
}
|
|
106
150
|
|
|
107
151
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=model-service.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"model-service.test.d.ts","sourceRoot":"","sources":["../../../../../src/test/domain/services/model-service.test.ts"],"names":[],"mappings":""}
|
|
File without changes
|