@declaro/data 2.0.0-beta.127 → 2.0.0-beta.129

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.
@@ -29,6 +29,25 @@ export declare class ModelService<TSchema extends AnyModelSchema> extends ReadOn
29
29
  * @returns The normalized input data.
30
30
  */
31
31
  protected normalizeInput(input: InferInput<TSchema>, args: INormalizeInputArgs<TSchema>): Promise<InferInput<TSchema>>;
32
+ /**
33
+ * Converts a detail object to a valid input for this service's schema.
34
+ * Picks only fields that exist in the input model and validates/coerces
35
+ * them through the input schema. Useful for duplicating entities or
36
+ * converting a detail from one entity type into an input for another.
37
+ * @param detail The detail object to convert (can be from any schema).
38
+ * @returns A validated input object with coerced values.
39
+ */
40
+ detailsToInput(detail: Record<string, unknown>): Promise<InferInput<TSchema>>;
41
+ /**
42
+ * Duplicates an existing entity by loading it, converting to input, removing the
43
+ * primary key, and creating a new record. Accepts an optional partial input to
44
+ * merge on top of the converted copy before creation.
45
+ * @param lookup The lookup criteria to find the record to duplicate.
46
+ * @param overrides Optional partial input to merge on top of the duplicated data.
47
+ * @param options Optional create options.
48
+ * @returns The newly created duplicate record.
49
+ */
50
+ duplicate(lookup: InferLookup<TSchema>, overrides?: Partial<InferInput<TSchema>>, options?: ICreateOptions): Promise<InferDetail<TSchema>>;
32
51
  /**
33
52
  * Removes a record by its lookup criteria.
34
53
  * @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,gBAAgB,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EACR,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,EACf,MAAM,qCAAqC,CAAA;AAG5C,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;IAClD;;OAEG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAChC;AACD,MAAM,WAAW,cAAe,SAAQ,cAAc;IAClD;;OAEG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,cAAc;IAC/D,QAAQ,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IAC/B,UAAU,EAAE,gBAAgB,CAAA;CAC/B;AAED,qBAAa,YAAY,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,oBAAoB,CAAC,OAAO,CAAC;gBAC/E,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;IAI5C;;;;;;OAMG;cACa,cAAc,CAC1B,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAC1B,IAAI,EAAE,mBAAmB,CAAC,OAAO,CAAC,GACnC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAI/B;;;;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;IA+B3F,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;IAiChC;;;;;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;IAiElH;;;;;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;IAiIlC;;;;OAIG;IACG,UAAU,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBlE;;;;OAIG;IACG,0BAA0B,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAsB9F;;;;OAIG;IACG,iBAAiB,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;CAqBxF"}
1
+ {"version":3,"file":"model-service.d.ts","sourceRoot":"","sources":["../../../../src/domain/services/model-service.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.127",
3
+ "version": "2.0.0-beta.129",
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.127",
26
- "@declaro/core": "^2.0.0-beta.127",
27
- "@declaro/zod": "^2.0.0-beta.127",
25
+ "@declaro/auth": "^2.0.0-beta.129",
26
+ "@declaro/core": "^2.0.0-beta.129",
27
+ "@declaro/zod": "^2.0.0-beta.129",
28
28
  "crypto-browserify": "^3.12.1",
29
29
  "typescript": "^5.8.3",
30
30
  "uuid": "^11.1.0",
@@ -37,11 +37,11 @@
37
37
  "src/**/*"
38
38
  ],
39
39
  "exports": {
40
- "bun": "./src/index.ts",
41
40
  "types": "./dist/ts/index.d.ts",
42
41
  "import": "./dist/node/index.js",
43
42
  "require": "./dist/node/index.cjs",
44
- "browser": "./dist/browser/index.js"
43
+ "browser": "./dist/browser/index.js",
44
+ "default": "./dist/node/index.js"
45
45
  },
46
- "gitHead": "202022ddd09776b9e2e02daac5af2359191ae108"
46
+ "gitHead": "b50c976a2f294d82d3d1d03a71cb6c6053a114df"
47
47
  }
@@ -935,6 +935,186 @@ describe('ModelService', () => {
935
935
  })
936
936
  })
937
937
 
938
+ describe('detailsToInput', () => {
939
+ it('should convert a detail object to an input by picking only input fields', async () => {
940
+ const detail = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
941
+ const input = await service.detailsToInput(detail)
942
+
943
+ expect(input.title).toBe('Test Book')
944
+ expect(input.author).toBe('Author Name')
945
+ expect(input.publishedDate).toEqual(new Date('2023-06-15'))
946
+ })
947
+
948
+ it('should exclude fields not present in the input model', async () => {
949
+ const detailWithExtras = {
950
+ id: 42,
951
+ title: 'Test Book',
952
+ author: 'Author Name',
953
+ publishedDate: new Date('2023-06-15'),
954
+ extraField: 'should be excluded',
955
+ anotherExtra: 123,
956
+ }
957
+ const input = await service.detailsToInput(detailWithExtras)
958
+
959
+ expect(input.title).toBe('Test Book')
960
+ expect(input.author).toBe('Author Name')
961
+ expect((input as any).extraField).toBeUndefined()
962
+ expect((input as any).anotherExtra).toBeUndefined()
963
+ })
964
+
965
+ it('should coerce values through the input schema', async () => {
966
+ // publishedDate uses z.coerce.date(), so a string should be coerced to a Date
967
+ const detailWithStringDate = {
968
+ id: 42,
969
+ title: 'Test Book',
970
+ author: 'Author Name',
971
+ publishedDate: '2023-06-15T00:00:00.000Z',
972
+ }
973
+ const input = await service.detailsToInput(detailWithStringDate)
974
+
975
+ expect(input.publishedDate).toBeInstanceOf(Date)
976
+ expect(input.publishedDate).toEqual(new Date('2023-06-15T00:00:00.000Z'))
977
+ })
978
+
979
+ it('should include the primary key field when present in the source', async () => {
980
+ const detail = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date() }
981
+ const input = await service.detailsToInput(detail)
982
+
983
+ // id is optional in the input model but should be included if present
984
+ expect(input.id).toBe(42)
985
+ })
986
+
987
+ it('should work with partial source data that satisfies required input fields', async () => {
988
+ const minimalDetail = {
989
+ title: 'Minimal Book',
990
+ author: 'Some Author',
991
+ publishedDate: new Date(),
992
+ }
993
+ const input = await service.detailsToInput(minimalDetail)
994
+
995
+ expect(input.title).toBe('Minimal Book')
996
+ expect(input.author).toBe('Some Author')
997
+ expect(input.id).toBeUndefined()
998
+ })
999
+
1000
+ it('should produce a valid input that can be used with create', async () => {
1001
+ const detail = { id: 42, title: 'Test Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1002
+ const input = await service.detailsToInput(detail)
1003
+
1004
+ // Remove the primary key and create a new record
1005
+ delete (input as any).id
1006
+ const created = await service.create(input)
1007
+
1008
+ expect(created.title).toBe('Test Book')
1009
+ expect(created.author).toBe('Author Name')
1010
+ expect(created.id).toBeDefined()
1011
+ expect(created.id).not.toBe(42)
1012
+ })
1013
+ })
1014
+
1015
+ describe('duplicate', () => {
1016
+ it('should create a duplicate of an existing record with a new primary key', async () => {
1017
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1018
+ await repository.create(original)
1019
+
1020
+ const duplicate = await service.duplicate({ id: 42 })
1021
+
1022
+ expect(duplicate.title).toBe('Original Book')
1023
+ expect(duplicate.author).toBe('Author Name')
1024
+ expect(duplicate.publishedDate).toEqual(new Date('2023-06-15'))
1025
+ expect(duplicate.id).toBeDefined()
1026
+ expect(duplicate.id).not.toBe(42)
1027
+ })
1028
+
1029
+ it('should apply overrides to the duplicated record', async () => {
1030
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1031
+ await repository.create(original)
1032
+
1033
+ const duplicate = await service.duplicate({ id: 42 }, { title: 'Duplicated Book' })
1034
+
1035
+ expect(duplicate.title).toBe('Duplicated Book')
1036
+ expect(duplicate.author).toBe('Author Name')
1037
+ expect(duplicate.id).toBeDefined()
1038
+ expect(duplicate.id).not.toBe(42)
1039
+ })
1040
+
1041
+ it('should throw an error when trying to duplicate a non-existent record', async () => {
1042
+ await expect(service.duplicate({ id: 999 })).rejects.toThrow()
1043
+ })
1044
+
1045
+ it('should trigger create events for the duplicate', async () => {
1046
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date() }
1047
+ await repository.create(original)
1048
+
1049
+ beforeCreateSpy.mockClear()
1050
+ afterCreateSpy.mockClear()
1051
+
1052
+ await service.duplicate({ id: 42 })
1053
+
1054
+ expect(beforeCreateSpy).toHaveBeenCalledTimes(1)
1055
+ expect(afterCreateSpy).toHaveBeenCalledTimes(1)
1056
+ })
1057
+
1058
+ it('should not modify the original record', async () => {
1059
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1060
+ await repository.create(original)
1061
+
1062
+ await service.duplicate({ id: 42 }, { title: 'Modified Title' })
1063
+
1064
+ const loaded = await repository.load({ id: 42 })
1065
+ expect(loaded?.title).toBe('Original Book')
1066
+ expect(loaded?.author).toBe('Author Name')
1067
+ })
1068
+
1069
+ it('should allow overriding multiple fields', async () => {
1070
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1071
+ await repository.create(original)
1072
+
1073
+ const newDate = new Date('2024-01-01')
1074
+ const duplicate = await service.duplicate({ id: 42 }, {
1075
+ title: 'New Title',
1076
+ author: 'New Author',
1077
+ publishedDate: newDate,
1078
+ })
1079
+
1080
+ expect(duplicate.title).toBe('New Title')
1081
+ expect(duplicate.author).toBe('New Author')
1082
+ expect(duplicate.publishedDate).toEqual(newDate)
1083
+ expect(duplicate.id).not.toBe(42)
1084
+ })
1085
+
1086
+ it('should create independent records that can be modified separately', async () => {
1087
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date('2023-06-15') }
1088
+ await repository.create(original)
1089
+
1090
+ const duplicate = await service.duplicate({ id: 42 })
1091
+
1092
+ // Update the duplicate
1093
+ await service.update({ id: duplicate.id }, { title: 'Updated Duplicate', author: 'Updated Author', publishedDate: new Date() })
1094
+
1095
+ // Verify original is unchanged
1096
+ const loadedOriginal = await repository.load({ id: 42 })
1097
+ expect(loadedOriginal?.title).toBe('Original Book')
1098
+
1099
+ // Verify duplicate was updated
1100
+ const loadedDuplicate = await repository.load({ id: duplicate.id })
1101
+ expect(loadedDuplicate?.title).toBe('Updated Duplicate')
1102
+ })
1103
+
1104
+ it('should respect doNotDispatchEvents option', async () => {
1105
+ const original = { id: 42, title: 'Original Book', author: 'Author Name', publishedDate: new Date() }
1106
+ await repository.create(original)
1107
+
1108
+ beforeCreateSpy.mockClear()
1109
+ afterCreateSpy.mockClear()
1110
+
1111
+ await service.duplicate({ id: 42 }, undefined, { doNotDispatchEvents: true })
1112
+
1113
+ expect(beforeCreateSpy).not.toHaveBeenCalled()
1114
+ expect(afterCreateSpy).not.toHaveBeenCalled()
1115
+ })
1116
+ })
1117
+
938
1118
  describe('doNotDispatchEvents option', () => {
939
1119
  const beforeLoadSpy = mock(() => {})
940
1120
  const afterLoadSpy = mock(() => {})
@@ -1,4 +1,4 @@
1
- import type { ActionDescriptor, AnyModelSchema, IActionDescriptor } from '@declaro/core'
1
+ import type { ActionDescriptor, AnyModelSchema, IActionDescriptor, IAnyModel } from '@declaro/core'
2
2
  import type {
3
3
  InferDetail,
4
4
  InferFilters,
@@ -49,6 +49,72 @@ export class ModelService<TSchema extends AnyModelSchema> extends ReadOnlyModelS
49
49
  return input
50
50
  }
51
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
+
52
118
  /**
53
119
  * Removes a record by its lookup criteria.
54
120
  * @param lookup The lookup criteria to find the record.