@declaro/data 2.0.0-beta.14 → 2.0.0-beta.140

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.
Files changed (142) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +0 -0
  3. package/dist/browser/index.js +26 -0
  4. package/dist/browser/index.js.map +93 -0
  5. package/dist/node/index.cjs +13372 -0
  6. package/dist/node/index.cjs.map +93 -0
  7. package/dist/node/index.js +13351 -0
  8. package/dist/node/index.js.map +93 -0
  9. package/dist/ts/application/model-controller.d.ts +60 -0
  10. package/dist/ts/application/model-controller.d.ts.map +1 -0
  11. package/dist/ts/application/model-controller.test.d.ts +2 -0
  12. package/dist/ts/application/model-controller.test.d.ts.map +1 -0
  13. package/dist/ts/application/read-only-model-controller.d.ts +24 -0
  14. package/dist/ts/application/read-only-model-controller.d.ts.map +1 -0
  15. package/dist/ts/application/read-only-model-controller.test.d.ts +2 -0
  16. package/dist/ts/application/read-only-model-controller.test.d.ts.map +1 -0
  17. package/dist/ts/domain/events/domain-event.d.ts +41 -0
  18. package/dist/ts/domain/events/domain-event.d.ts.map +1 -0
  19. package/dist/ts/domain/events/domain-event.test.d.ts +2 -0
  20. package/dist/ts/domain/events/domain-event.test.d.ts.map +1 -0
  21. package/dist/ts/domain/events/event-types.d.ts +37 -0
  22. package/dist/ts/domain/events/event-types.d.ts.map +1 -0
  23. package/dist/ts/domain/events/mutation-event.d.ts +41 -0
  24. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -0
  25. package/dist/ts/domain/events/mutation-event.test.d.ts +2 -0
  26. package/dist/ts/domain/events/mutation-event.test.d.ts.map +1 -0
  27. package/dist/ts/domain/events/query-event.d.ts +8 -0
  28. package/dist/ts/domain/events/query-event.d.ts.map +1 -0
  29. package/dist/ts/domain/events/query-event.test.d.ts +2 -0
  30. package/dist/ts/domain/events/query-event.test.d.ts.map +1 -0
  31. package/dist/ts/domain/events/request-event.d.ts +26 -0
  32. package/dist/ts/domain/events/request-event.d.ts.map +1 -0
  33. package/dist/ts/domain/events/request-event.test.d.ts +2 -0
  34. package/dist/ts/domain/events/request-event.test.d.ts.map +1 -0
  35. package/dist/ts/domain/interfaces/repository.d.ts +110 -0
  36. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -0
  37. package/dist/ts/domain/models/pagination.d.ts +28 -0
  38. package/dist/ts/domain/models/pagination.d.ts.map +1 -0
  39. package/dist/ts/domain/services/base-model-service.d.ts +23 -0
  40. package/dist/ts/domain/services/base-model-service.d.ts.map +1 -0
  41. package/dist/ts/domain/services/model-service-args.d.ts +9 -0
  42. package/dist/ts/domain/services/model-service-args.d.ts.map +1 -0
  43. package/dist/ts/domain/services/model-service.d.ts +99 -0
  44. package/dist/ts/domain/services/model-service.d.ts.map +1 -0
  45. package/dist/ts/domain/services/model-service.normalization.test.d.ts +2 -0
  46. package/dist/ts/domain/services/model-service.normalization.test.d.ts.map +1 -0
  47. package/dist/ts/domain/services/model-service.test.d.ts +2 -0
  48. package/dist/ts/domain/services/model-service.test.d.ts.map +1 -0
  49. package/dist/ts/domain/services/read-only-model-service.d.ts +90 -0
  50. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -0
  51. package/dist/ts/domain/services/read-only-model-service.test.d.ts +2 -0
  52. package/dist/ts/domain/services/read-only-model-service.test.d.ts.map +1 -0
  53. package/dist/ts/index.d.ts +18 -0
  54. package/dist/ts/index.d.ts.map +1 -0
  55. package/dist/ts/shared/utils/schema-inference.d.ts +23 -0
  56. package/dist/ts/shared/utils/schema-inference.d.ts.map +1 -0
  57. package/dist/ts/shared/utils/schema-inheritance.d.ts +24 -0
  58. package/dist/ts/shared/utils/schema-inheritance.d.ts.map +1 -0
  59. package/dist/ts/shared/utils/schema-inheritance.test.d.ts +2 -0
  60. package/dist/ts/shared/utils/schema-inheritance.test.d.ts.map +1 -0
  61. package/dist/ts/shared/utils/test/animal-schema.d.ts +57 -0
  62. package/dist/ts/shared/utils/test/animal-schema.d.ts.map +1 -0
  63. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts +55 -0
  64. package/dist/ts/shared/utils/test/animal-trait-schema.d.ts.map +1 -0
  65. package/dist/ts/shared/utils/test/elephant-schema.d.ts +30 -0
  66. package/dist/ts/shared/utils/test/elephant-schema.d.ts.map +1 -0
  67. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts +26 -0
  68. package/dist/ts/shared/utils/test/elephant-trait-schema.d.ts.map +1 -0
  69. package/dist/ts/test/mock/models/mock-book-models.d.ts +42 -0
  70. package/dist/ts/test/mock/models/mock-book-models.d.ts.map +1 -0
  71. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts +2 -0
  72. package/dist/ts/test/mock/repositories/mock-memory-repository.assign.test.d.ts.map +1 -0
  73. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts +2 -0
  74. package/dist/ts/test/mock/repositories/mock-memory-repository.basic.test.d.ts.map +1 -0
  75. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts +2 -0
  76. package/dist/ts/test/mock/repositories/mock-memory-repository.bulk-upsert.test.d.ts.map +1 -0
  77. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts +2 -0
  78. package/dist/ts/test/mock/repositories/mock-memory-repository.count.test.d.ts.map +1 -0
  79. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +62 -0
  80. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -0
  81. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts +2 -0
  82. package/dist/ts/test/mock/repositories/mock-memory-repository.search.test.d.ts.map +1 -0
  83. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  84. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  85. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts +2 -0
  86. package/dist/ts/test/mock/repositories/mock-memory-repository.upsert.test.d.ts.map +1 -0
  87. package/package.json +46 -42
  88. package/src/application/model-controller.test.ts +694 -0
  89. package/src/application/model-controller.ts +186 -0
  90. package/src/application/read-only-model-controller.test.ts +335 -0
  91. package/src/application/read-only-model-controller.ts +79 -0
  92. package/src/domain/events/domain-event.test.ts +82 -0
  93. package/src/domain/events/domain-event.ts +69 -0
  94. package/src/domain/events/event-types.ts +37 -0
  95. package/src/domain/events/mutation-event.test.ts +390 -0
  96. package/src/domain/events/mutation-event.ts +53 -0
  97. package/src/domain/events/query-event.test.ts +228 -0
  98. package/src/domain/events/query-event.ts +14 -0
  99. package/src/domain/events/request-event.test.ts +38 -0
  100. package/src/domain/events/request-event.ts +47 -0
  101. package/src/domain/interfaces/repository.ts +136 -0
  102. package/src/domain/models/pagination.ts +28 -0
  103. package/src/domain/services/base-model-service.ts +54 -0
  104. package/src/domain/services/model-service-args.ts +9 -0
  105. package/src/domain/services/model-service.normalization.test.ts +704 -0
  106. package/src/domain/services/model-service.test.ts +1616 -0
  107. package/src/domain/services/model-service.ts +597 -0
  108. package/src/domain/services/read-only-model-service.test.ts +1130 -0
  109. package/src/domain/services/read-only-model-service.ts +211 -0
  110. package/src/index.ts +17 -4
  111. package/src/shared/utils/schema-inference.ts +26 -0
  112. package/src/shared/utils/schema-inheritance.test.ts +295 -0
  113. package/src/shared/utils/schema-inheritance.ts +28 -0
  114. package/src/shared/utils/test/animal-schema.ts +46 -0
  115. package/src/shared/utils/test/animal-trait-schema.ts +45 -0
  116. package/src/shared/utils/test/elephant-schema.ts +58 -0
  117. package/src/shared/utils/test/elephant-trait-schema.ts +53 -0
  118. package/src/test/mock/models/mock-book-models.ts +78 -0
  119. package/src/test/mock/repositories/mock-memory-repository.assign.test.ts +215 -0
  120. package/src/test/mock/repositories/mock-memory-repository.basic.test.ts +129 -0
  121. package/src/test/mock/repositories/mock-memory-repository.bulk-upsert.test.ts +159 -0
  122. package/src/test/mock/repositories/mock-memory-repository.count.test.ts +98 -0
  123. package/src/test/mock/repositories/mock-memory-repository.search.test.ts +265 -0
  124. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  125. package/src/test/mock/repositories/mock-memory-repository.ts +401 -0
  126. package/src/test/mock/repositories/mock-memory-repository.upsert.test.ts +108 -0
  127. package/dist/databaseConnection.d.ts +0 -24
  128. package/dist/datastoreAbstract.d.ts +0 -37
  129. package/dist/declaro-data.cjs +0 -1
  130. package/dist/declaro-data.mjs +0 -250
  131. package/dist/hydrateEntity.d.ts +0 -8
  132. package/dist/index.d.ts +0 -4
  133. package/dist/serverConnection.d.ts +0 -15
  134. package/dist/trackedStatus.d.ts +0 -15
  135. package/src/databaseConnection.ts +0 -137
  136. package/src/datastoreAbstract.ts +0 -190
  137. package/src/hydrateEntity.ts +0 -36
  138. package/src/placeholder.test.ts +0 -7
  139. package/src/serverConnection.ts +0 -74
  140. package/src/trackedStatus.ts +0 -35
  141. package/tsconfig.json +0 -10
  142. package/vite.config.ts +0 -23
@@ -0,0 +1,401 @@
1
+ import type { AnyModelSchema, IModelEntityMetadata, JSONSchema, Model } from '@declaro/core'
2
+ import type { IRepository } from '../../../domain/interfaces/repository'
3
+ import type { IPaginationInput } from '../../../domain/models/pagination'
4
+ import type {
5
+ InferDetail,
6
+ InferFilters,
7
+ InferInput,
8
+ InferLookup,
9
+ InferSearchResults,
10
+ InferSummary,
11
+ } from '../../../shared/utils/schema-inference'
12
+ import { v4 as uuid } from 'uuid'
13
+ import type { ILoadOptions, ISearchOptions } from '../../../domain/services/read-only-model-service'
14
+ import type { ICreateOptions, IUpdateOptions } from '../../../domain/services/model-service'
15
+
16
+ export interface IMockMemoryRepositoryArgs<TSchema extends AnyModelSchema> {
17
+ schema: TSchema
18
+ lookup?: (data: InferDetail<TSchema>, lookup: InferLookup<TSchema>) => boolean
19
+ filter?: (data: InferSummary<TSchema>, filters: InferFilters<TSchema>) => boolean
20
+ assign?: (data: InferDetail<TSchema>, input: InferInput<TSchema>) => InferDetail<TSchema>
21
+ }
22
+
23
+ export class MockMemoryRepository<TSchema extends AnyModelSchema> implements IRepository<TSchema> {
24
+ protected data = new Map<string, InferDetail<TSchema>>()
25
+ protected trash = new Map<string, InferDetail<TSchema>>()
26
+ protected entityMetadata: IModelEntityMetadata
27
+ protected nextId: number = 0
28
+
29
+ constructor(protected args: IMockMemoryRepositoryArgs<TSchema>) {
30
+ this.entityMetadata = this.args.schema.getEntityMetadata()
31
+ if (!this.entityMetadata?.primaryKey) {
32
+ throw new Error('Primary key must be specified for MockMemoryRepository')
33
+ }
34
+ }
35
+
36
+ private findOne(
37
+ lookup: InferLookup<TSchema>,
38
+ map: Map<string, InferDetail<TSchema>>,
39
+ ): InferDetail<TSchema> | undefined {
40
+ if (typeof this.args.lookup === 'function') {
41
+ return Array.from(map.values()).find((data) => this.args.lookup!(data, lookup))
42
+ } else {
43
+ // Default lookup by primary key
44
+ return map.get(lookup[this.entityMetadata.primaryKey])
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Find an item and return both the item and its key
50
+ * @param lookup - The lookup criteria
51
+ * @param map - The map to search in
52
+ * @returns Object containing the item and its key, or undefined if not found
53
+ */
54
+ private findOneWithKey(
55
+ lookup: InferLookup<TSchema>,
56
+ map: Map<string, InferDetail<TSchema>>,
57
+ ): { item: InferDetail<TSchema>; key: string } | undefined {
58
+ if (typeof this.args.lookup === 'function') {
59
+ const item = Array.from(map.values()).find((data) => this.args.lookup!(data, lookup))
60
+ if (item) {
61
+ return { item, key: item[this.entityMetadata.primaryKey!] }
62
+ }
63
+ } else {
64
+ // Default lookup by primary key
65
+ const key = lookup[this.entityMetadata.primaryKey]
66
+ const item = map.get(key)
67
+ if (item) {
68
+ return { item, key }
69
+ }
70
+ }
71
+ return undefined
72
+ }
73
+
74
+ /**
75
+ * Loads a single item by lookup criteria.
76
+ * @param input - The lookup criteria.
77
+ * @param options - Optional load options including removedOnly and includeRemoved.
78
+ * @returns The found item or null if not found.
79
+ */
80
+ async load(input: InferLookup<TSchema>, options: ILoadOptions = {}): Promise<InferDetail<TSchema> | null> {
81
+ if (!this.entityMetadata?.primaryKey) {
82
+ throw new Error('Primary key is not defined in the schema metadata')
83
+ }
84
+
85
+ let item: InferDetail<TSchema> | undefined
86
+
87
+ if (options.removedOnly) {
88
+ item = this.findOne(input, this.trash)
89
+ } else if (options.includeRemoved) {
90
+ item = this.findOne(input, this.data) ?? this.findOne(input, this.trash)
91
+ } else {
92
+ item = this.findOne(input, this.data)
93
+ }
94
+
95
+ return item || null
96
+ }
97
+ async loadMany(inputs: InferLookup<TSchema>[]): Promise<InferDetail<TSchema>[]> {
98
+ if (!this.entityMetadata?.primaryKey) {
99
+ throw new Error('Primary key is not defined in the schema metadata')
100
+ }
101
+
102
+ const results: InferDetail<TSchema>[] = []
103
+ for (const input of inputs) {
104
+ let item: InferDetail<TSchema> | undefined
105
+ if (typeof this.args.lookup === 'function') {
106
+ item = Array.from(this.data.values()).find((data) => this.args.lookup!(data, input))
107
+ } else {
108
+ // Default lookup by primary key
109
+ item = this.data.get(input[this.entityMetadata.primaryKey!])
110
+ }
111
+ if (item) {
112
+ results.push(item)
113
+ }
114
+ }
115
+ return results
116
+ }
117
+ async search(
118
+ input: InferFilters<TSchema>,
119
+ options?: ISearchOptions<TSchema>,
120
+ ): Promise<InferSearchResults<TSchema>> {
121
+ const pagination = options?.pagination || { page: 1, pageSize: 25 }
122
+ let items = this.applyFilters(input, options)
123
+
124
+ // Apply sorting if provided
125
+ if (options?.sort && Array.isArray(options.sort)) {
126
+ items = items.sort((a, b) => {
127
+ for (const sortField of options.sort! as any[]) {
128
+ for (const [field, direction] of Object.entries(sortField)) {
129
+ if (!direction || typeof direction !== 'string') continue
130
+
131
+ const aValue = a[field as keyof typeof a]
132
+ const bValue = b[field as keyof typeof b]
133
+
134
+ let comparison = 0
135
+ if (aValue < bValue) comparison = -1
136
+ else if (aValue > bValue) comparison = 1
137
+
138
+ if (comparison !== 0) {
139
+ // Handle different sort directions
140
+ const isDesc = direction.includes('desc')
141
+ return isDesc ? -comparison : comparison
142
+ }
143
+ }
144
+ }
145
+ return 0
146
+ })
147
+ }
148
+
149
+ return {
150
+ results: items.slice(
151
+ ((pagination?.page ?? 1) - 1) * (pagination?.pageSize ?? 25),
152
+ (pagination?.page ?? 1) * (pagination?.pageSize ?? 25),
153
+ ),
154
+ pagination: {
155
+ total: items.length,
156
+ totalPages: Math.ceil(items.length / (pagination?.pageSize ?? 25)),
157
+ ...pagination,
158
+ page: pagination?.page ?? 1,
159
+ pageSize: pagination?.pageSize ?? 25,
160
+ },
161
+ }
162
+ }
163
+ async remove(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
164
+ if (!this.entityMetadata?.primaryKey) {
165
+ throw new Error('Primary key is not defined in the schema metadata')
166
+ }
167
+
168
+ const found = this.findOneWithKey(lookup, this.data)
169
+ if (!found) {
170
+ throw new Error('Item not found')
171
+ }
172
+
173
+ // Move the item to trash
174
+ this.trash.set(found.key, found.item)
175
+ // Remove the item from data
176
+ this.data.delete(found.key)
177
+ return found.item
178
+ }
179
+ async restore(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
180
+ if (!this.entityMetadata?.primaryKey) {
181
+ throw new Error('Primary key is not defined in the schema metadata')
182
+ }
183
+
184
+ const found = this.findOneWithKey(lookup, this.trash)
185
+ if (!found) {
186
+ throw new Error('Item not found in trash')
187
+ }
188
+
189
+ this.trash.delete(found.key)
190
+ this.data.set(found.key, found.item)
191
+ return found.item
192
+ }
193
+
194
+ async create(input: InferInput<TSchema>): Promise<InferDetail<TSchema>> {
195
+ if (!this.entityMetadata?.primaryKey) {
196
+ throw new Error('Primary key is not defined in the schema metadata')
197
+ }
198
+ const primaryKeyValue = input[this.entityMetadata.primaryKey]
199
+
200
+ if (primaryKeyValue && this.data.has(primaryKeyValue)) {
201
+ throw new Error('Item with the same primary key already exists')
202
+ }
203
+
204
+ const baseData = {} as InferDetail<TSchema>
205
+ const payload = this.assignInput(baseData, input)
206
+
207
+ if (!payload[this.entityMetadata.primaryKey]) {
208
+ // Generate a new primary key if not provided
209
+ payload[this.entityMetadata.primaryKey!] = await this.generatePrimaryKey()
210
+ }
211
+
212
+ this.data.set(payload[this.entityMetadata.primaryKey!], payload)
213
+ return payload
214
+ }
215
+
216
+ async update(lookup: InferLookup<TSchema>, input: InferInput<TSchema>): Promise<InferDetail<TSchema>> {
217
+ if (!this.entityMetadata?.primaryKey) {
218
+ throw new Error('Primary key is not defined in the schema metadata')
219
+ }
220
+
221
+ let existingItem: InferDetail<TSchema> | undefined
222
+ let itemKey: string
223
+
224
+ if (typeof this.args.lookup === 'function') {
225
+ existingItem = Array.from(this.data.values()).find((data) => this.args.lookup!(data, lookup))
226
+ if (existingItem) {
227
+ itemKey = existingItem[this.entityMetadata.primaryKey!]
228
+ } else {
229
+ throw new Error('Item not found')
230
+ }
231
+ } else {
232
+ // Default lookup by primary key
233
+ itemKey = lookup[this.entityMetadata.primaryKey]
234
+ if (!itemKey) {
235
+ throw new Error('Primary key value must be provided')
236
+ }
237
+ existingItem = this.data.get(itemKey)
238
+ if (!existingItem) {
239
+ throw new Error('Item not found')
240
+ }
241
+ }
242
+
243
+ const updatedItem = this.assignInput(existingItem, input)
244
+ this.data.set(itemKey!, updatedItem)
245
+ return updatedItem
246
+ }
247
+
248
+ async count(search: InferFilters<TSchema>, options?: ISearchOptions<TSchema> | undefined): Promise<number> {
249
+ const filteredItems = this.applyFilters(search, options)
250
+ return filteredItems.length
251
+ }
252
+
253
+ async upsert(input: InferInput<TSchema>, options?: ICreateOptions | IUpdateOptions): Promise<InferDetail<TSchema>> {
254
+ const primaryKeyValue = input[this.entityMetadata.primaryKey]
255
+ let existingItem: InferDetail<TSchema> = {} as InferDetail<TSchema>
256
+
257
+ if (primaryKeyValue) {
258
+ existingItem = this.data.get(primaryKeyValue) ?? ({} as InferDetail<TSchema>)
259
+ }
260
+
261
+ const updatedItem = this.assignInput(existingItem, input)
262
+ if (!updatedItem[this.entityMetadata.primaryKey!]) {
263
+ updatedItem[this.entityMetadata.primaryKey!] = await this.generatePrimaryKey()
264
+ }
265
+
266
+ this.data.set(updatedItem[this.entityMetadata.primaryKey!], updatedItem)
267
+
268
+ return updatedItem
269
+ }
270
+
271
+ async bulkUpsert(
272
+ inputs: InferInput<TSchema>[],
273
+ options?: ICreateOptions | IUpdateOptions,
274
+ ): Promise<InferDetail<TSchema>[]> {
275
+ return await Promise.all(inputs.map((input) => this.upsert(input, options)))
276
+ }
277
+
278
+ async permanentlyDelete(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
279
+ if (!this.entityMetadata?.primaryKey) {
280
+ throw new Error('Primary key is not defined in the schema metadata')
281
+ }
282
+
283
+ // Try to find in main data first, then trash
284
+ const foundInData = this.findOneWithKey(lookup, this.data)
285
+ if (foundInData) {
286
+ this.data.delete(foundInData.key)
287
+ return foundInData.item
288
+ }
289
+
290
+ const foundInTrash = this.findOneWithKey(lookup, this.trash)
291
+ if (foundInTrash) {
292
+ this.trash.delete(foundInTrash.key)
293
+ return foundInTrash.item
294
+ }
295
+
296
+ throw new Error('Item not found')
297
+ }
298
+
299
+ async permanentlyDeleteFromTrash(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
300
+ if (!this.entityMetadata?.primaryKey) {
301
+ throw new Error('Primary key is not defined in the schema metadata')
302
+ }
303
+
304
+ const found = this.findOneWithKey(lookup, this.trash)
305
+ if (!found) {
306
+ throw new Error('Item not found in trash')
307
+ }
308
+
309
+ this.trash.delete(found.key)
310
+ return found.item
311
+ }
312
+
313
+ async emptyTrash(filters?: InferFilters<TSchema>): Promise<number> {
314
+ if (!filters || Object.keys(filters).length === 0) {
315
+ // Delete all items from trash
316
+ const count = this.trash.size
317
+ this.trash.clear()
318
+ return count
319
+ }
320
+
321
+ // Apply filters to trash items
322
+ const itemsToDelete: string[] = []
323
+ for (const [key, item] of this.trash.entries()) {
324
+ if (typeof this.args.filter === 'function') {
325
+ if (this.args.filter(item, filters)) {
326
+ itemsToDelete.push(key)
327
+ }
328
+ } else {
329
+ // No filter function provided - match all items
330
+ itemsToDelete.push(key)
331
+ }
332
+ }
333
+
334
+ // Delete filtered items
335
+ for (const key of itemsToDelete) {
336
+ this.trash.delete(key)
337
+ }
338
+
339
+ return itemsToDelete.length
340
+ }
341
+
342
+ /**
343
+ * Apply filtering logic to all items based on the provided search criteria
344
+ * @param input - The search/filter criteria
345
+ * @param options - Optional search options including removedOnly and includeRemoved
346
+ * @returns Filtered array of items
347
+ */
348
+ protected applyFilters(input: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): InferDetail<TSchema>[] {
349
+ let sourceItems: InferDetail<TSchema>[]
350
+
351
+ if (options?.removedOnly) {
352
+ // Only search in trash
353
+ sourceItems = Array.from(this.trash.values())
354
+ } else if (options?.includeRemoved) {
355
+ // Search in both data and trash
356
+ sourceItems = [...Array.from(this.data.values()), ...Array.from(this.trash.values())]
357
+ } else {
358
+ // Default: only search in active data
359
+ sourceItems = Array.from(this.data.values())
360
+ }
361
+
362
+ return sourceItems.filter((item) => {
363
+ // Apply filtering logic based on the input
364
+ if (typeof this.args.filter === 'function') {
365
+ return this.args.filter(item, input)
366
+ } else {
367
+ return true
368
+ }
369
+ })
370
+ }
371
+
372
+ /**
373
+ * Assign input data to existing data using the provided assign function or default Object.assign
374
+ * @param existingData - The existing data to merge with
375
+ * @param input - The input data to assign
376
+ * @returns The merged data
377
+ */
378
+ protected assignInput(existingData: InferDetail<TSchema>, input: InferInput<TSchema>): InferDetail<TSchema> {
379
+ if (typeof this.args.assign === 'function') {
380
+ return this.args.assign(existingData, input)
381
+ } else {
382
+ // Default implementation using Object.assign
383
+ return Object.assign({}, existingData, input) as InferDetail<TSchema>
384
+ }
385
+ }
386
+
387
+ protected async generatePrimaryKey() {
388
+ const lookupModel: Model<any, any> = this.args.schema.definition.lookup
389
+ const lookupMeta = await lookupModel.toJSONSchema()
390
+ const primaryKeyMeta = lookupMeta.properties?.[this.entityMetadata.primaryKey!] as JSONSchema
391
+ const type = primaryKeyMeta.type as string
392
+
393
+ if (type === 'string') {
394
+ return uuid()
395
+ } else if (['number', 'integer'].includes(type)) {
396
+ return ++this.nextId
397
+ } else {
398
+ throw new Error(`Unsupported primary key type: ${type}`)
399
+ }
400
+ }
401
+ }
@@ -0,0 +1,108 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { MockBookSchema } from '../models/mock-book-models'
3
+ import { MockMemoryRepository } from './mock-memory-repository'
4
+
5
+ describe('MockMemoryRepository - Upsert Functionality', () => {
6
+ const mockSchema = MockBookSchema
7
+
8
+ let repository: MockMemoryRepository<typeof mockSchema>
9
+
10
+ beforeEach(() => {
11
+ repository = new MockMemoryRepository({ schema: mockSchema })
12
+ })
13
+
14
+ it('should create a new item when no existing item with primary key exists', async () => {
15
+ const input = { id: 42, title: 'New Book', author: 'Author Name', publishedDate: new Date() }
16
+
17
+ const upsertedItem = await repository.upsert(input)
18
+
19
+ expect(upsertedItem).toEqual(input)
20
+ expect(await repository.load({ id: 42 })).toEqual(input)
21
+ })
22
+
23
+ it('should update an existing item when primary key matches', async () => {
24
+ // Create initial item
25
+ const initial = {
26
+ id: 42,
27
+ title: 'Original Book',
28
+ author: 'Original Author',
29
+ publishedDate: new Date('2023-01-01'),
30
+ }
31
+ await repository.create(initial)
32
+
33
+ // Upsert with same ID but different data
34
+ const update = {
35
+ id: 42,
36
+ title: 'Updated Book',
37
+ author: 'Updated Author',
38
+ publishedDate: new Date('2023-12-01'),
39
+ }
40
+ const upsertedItem = await repository.upsert(update)
41
+
42
+ expect(upsertedItem).toEqual(update)
43
+ expect(await repository.load({ id: 42 })).toEqual(update)
44
+ })
45
+
46
+ it('should generate primary key when upserting without one', async () => {
47
+ const input = { title: 'Book Without ID', author: 'Author Name', publishedDate: new Date() }
48
+
49
+ const upsertedItem = await repository.upsert(input)
50
+
51
+ expect(upsertedItem.id).toBeDefined()
52
+ expect(upsertedItem.title).toBe(input.title)
53
+ expect(upsertedItem.author).toBe(input.author)
54
+ expect(await repository.load({ id: upsertedItem.id })).toEqual(upsertedItem)
55
+ })
56
+
57
+ it('should merge with existing item properties when updating', async () => {
58
+ // Create initial item with multiple properties
59
+ const initial = {
60
+ id: 42,
61
+ title: 'Original Book',
62
+ author: 'Original Author',
63
+ publishedDate: new Date('2023-01-01'),
64
+ }
65
+ await repository.create(initial)
66
+
67
+ // Upsert with partial update (only title) - need to provide required fields
68
+ const partialUpdate = {
69
+ id: 42,
70
+ title: 'Updated Title',
71
+ author: 'Original Author', // Keep original
72
+ publishedDate: new Date('2023-01-01'), // Keep original
73
+ }
74
+ const upsertedItem = await repository.upsert(partialUpdate)
75
+
76
+ // Should have updated title but kept other properties
77
+ expect(upsertedItem.id).toBe(42)
78
+ expect(upsertedItem.title).toBe('Updated Title')
79
+ expect(upsertedItem.author).toBe('Original Author')
80
+ expect(upsertedItem.publishedDate).toEqual(initial.publishedDate)
81
+ })
82
+
83
+ it('should handle upsert with null/undefined primary key', async () => {
84
+ const input = {
85
+ id: undefined,
86
+ title: 'Book With Undefined ID',
87
+ author: 'Author Name',
88
+ publishedDate: new Date(),
89
+ }
90
+
91
+ const upsertedItem = await repository.upsert(input)
92
+
93
+ expect(upsertedItem.id).toBeDefined()
94
+ expect(typeof upsertedItem.id).toBe('number')
95
+ expect(upsertedItem.title).toBe(input.title)
96
+ })
97
+
98
+ it('should increment auto-generated IDs correctly', async () => {
99
+ const input1 = { title: 'Book 1', author: 'Author 1', publishedDate: new Date() }
100
+ const input2 = { title: 'Book 2', author: 'Author 2', publishedDate: new Date() }
101
+
102
+ const item1 = await repository.upsert(input1)
103
+ const item2 = await repository.upsert(input2)
104
+
105
+ expect(item1.id).toBe(1)
106
+ expect(item2.id).toBe(2)
107
+ })
108
+ })
@@ -1,24 +0,0 @@
1
- import type { IDatastoreProvider, BaseModel, BaseModelClass } from '@declaro/core';
2
- import type { EntityManager, FilterQuery, Reference } from "@mikro-orm/core";
3
- import type { RemoveReturnType, UpsertReturnType } from "./datastoreAbstract";
4
- export type DatabaseConnectionOptions = {
5
- populate?: string[];
6
- immutableFields?: string[];
7
- };
8
- export declare class DatabaseConnection<T extends BaseModel<any>> implements IDatastoreProvider<T> {
9
- private repository;
10
- static inject: readonly ["EntityManager", "Reference"];
11
- readonly em: EntityManager;
12
- private hydrator;
13
- private populate;
14
- private immutableFields;
15
- constructor(em: EntityManager, reference: typeof Reference);
16
- setup(model: BaseModelClass<T>, options: DatabaseConnectionOptions): void;
17
- getAll(): Promise<void | any[]>;
18
- getWhere(filter?: FilterQuery<any>): Promise<void | any[]>;
19
- get(id: string | number): Promise<any>;
20
- upsert<T extends BaseModel<any> | BaseModel<any>[]>(data: T): Promise<UpsertReturnType<T>>;
21
- private singleUpsert;
22
- remove(data: T[] | T): Promise<RemoveReturnType>;
23
- private singleRemove;
24
- }
@@ -1,37 +0,0 @@
1
- import type { IDatastoreProvider, BaseModel, BaseModelClass, IStore } from "@declaro/core";
2
- import type { FetchFunc } from '@declaro/core';
3
- import { TrackedStatusStore } from "./trackedStatus";
4
- import type { FilterQuery } from "@mikro-orm/core";
5
- export type TrackedPayload<T> = {
6
- model: T;
7
- requestId: string;
8
- optimistic?: boolean;
9
- };
10
- export type UpsertReturnType<T> = T extends (infer U)[] ? U[] : T;
11
- export type RemoveReturnType = (number | string)[] | number | string | null;
12
- export declare abstract class AbstractStore<T extends BaseModel<any>> implements IStore {
13
- protected connection: IDatastoreProvider<T>;
14
- protected model: BaseModelClass<T>;
15
- protected options?: any;
16
- protected value: T[];
17
- private subscribers;
18
- private hydrated;
19
- trackedStatus: TrackedStatusStore;
20
- protected constructor(connection: IDatastoreProvider<T>, model: BaseModelClass<T>, options?: any);
21
- subscribe(subscription: (value: T[]) => void): (() => void);
22
- setFetch(fetch: FetchFunc): void;
23
- set(value: T[]): void;
24
- get(value: string | number): Promise<T>;
25
- getWhere(filter?: FilterQuery<any>): Promise<T[]>;
26
- getAll(): Promise<T[]>;
27
- hydrate(id?: string | number, filter?: FilterQuery<any>): Promise<void>;
28
- upsert(model: T | T[], optimistic?: boolean): Promise<UpsertReturnType<T>>;
29
- remove(model: T | T[], optimistic?: boolean): Promise<RemoveReturnType>;
30
- trackedUpsert(payload: TrackedPayload<T | T[]>): Promise<UpsertReturnType<T>>;
31
- trackedRemove(payload: TrackedPayload<T | T[]>): Promise<RemoveReturnType>;
32
- insertIntoStore(obj: T): void;
33
- removeFromStore(obj: T): void;
34
- }
35
- export type ActionableStore = AbstractStore<BaseModel<any>> & {
36
- [key: string]: (...args: any[]) => any;
37
- };
@@ -1 +0,0 @@
1
- "use strict";var u=Object.defineProperty;var l=(o,e,t)=>e in o?u(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var n=(o,e,t)=>(l(o,typeof e!="symbol"?e+"":e,t),t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class h{constructor(e,t){this.em=e,this.reference=t}hydrateEntity(e){const t=this.em.getMetadata().get(e.constructor.name);for(const s in e){const r=t.properties[s];if(e[s]!==void 0&&e[s]!==null&&r&&["m:1","1:1"].includes(r.reference)){const i=r.entity();e[s]=this.reference.createFromPK(i.meta.class,e[s])}}return e}}class c{constructor(e,t){n(this,"repository");n(this,"em");n(this,"hydrator");n(this,"populate");n(this,"immutableFields",[]);this.em=e,this.hydrator=new h(this.em,t)}setup(e,t){this.repository=this.em.getRepository(e),this.populate=t==null?void 0:t.populate,t!=null&&t.immutableFields&&(this.immutableFields=t.immutableFields)}getAll(){return this.repository.findAll({populate:this.populate}).catch(e=>{console.log(e)})}getWhere(e){return this.repository.find(e,{populate:this.populate}).catch(t=>{console.log(t)})}get(e){return this.repository.findOne(e,{populate:this.populate}).catch(t=>{console.log(t)})}async upsert(e){if(Array.isArray(e)){const t=[];for(const s of e){const r=await this.singleUpsert(s);t.push(r)}return t}else return await this.singleUpsert(e)}async singleUpsert(e){let t;const s=this.em.getMetadata().get(e.constructor.name),r={};return Object.keys(e).forEach(i=>{const a=s.properties[i];a&&a.reference!=="m:n"&&(r[i]=e[i])}),e.id?t=await this.em.findOneOrFail(e.constructor.name,e.id):(t=this.em.create(e.constructor.name,r),await this.em.persist(t).flush()),delete e.id,this.immutableFields.forEach(i=>{i in e&&(e[i]=t[i])}),this.em.assign(t,e),await this.em.persist(t).flush(),t}async remove(e){if(Array.isArray(e)){const t=[];for(const s of e)await this.singleRemove(s)&&t.push(s.id);return t}else return await this.singleRemove(e)?e.id:null}async singleRemove(e){try{const t=await this.em.findOneOrFail(e.constructor.name,e.id);return await this.em.remove(t).flush(),!0}catch(t){if(t.constructor.name==="NotFoundError")return!1;throw t}}}n(c,"inject",["EntityManager","Reference"]);class m{constructor(){n(this,"fetch");n(this,"model")}setup(e){this.model=e}setFetch(e){this.fetch=e}getAll(){return this.fetch(`/store/${this.model.name}/getAll`).then(e=>e.json().then(t=>t.map(s=>Object.assign(new this.model,s))))}getWhere(e){return this.fetch(`/store/${this.model.name}/getWhere/${JSON.stringify(e)}`).then(t=>t.json().then(s=>s.map(r=>Object.assign(new this.model,r))))}get(e){return this.fetch(`/store/${this.model.name}/get/${e}`).then(t=>t.json())}upsert(e){return this._callStoreMethod("upsert","POST",e)}remove(e){return this._callStoreMethod("remove","POST",e)}_callStoreMethod(e,t,s=null){let r={};return s&&(r["Content-Type"]="application/json;",r.Accept="application/json;"),this.fetch(`/store/${this.model.name}/${e}`,{method:t,body:s?JSON.stringify(s):null,headers:r}).then(async i=>{const a=await i.json();if(i.ok)return a;throw Error(a.message)})}}class f{constructor(){n(this,"value",{});n(this,"subscribers",[])}subscribe(e){return this.subscribers.push(e),e(this.value),()=>{this.subscribers=this.subscribers.filter(t=>t!==e)}}push(e){this.value={...this.value,[e.requestId]:e},this.subscribers.forEach(t=>t(this.value))}remove(e){const{[e]:t,...s}=this.value;this.value=s,this.subscribers.forEach(r=>r(this.value))}}class d{constructor(e,t,s){n(this,"value",[]);n(this,"subscribers",[]);n(this,"hydrated",!1);n(this,"trackedStatus",new f);this.connection=e,this.model=t,this.options=s,this.connection.setup(this.model,s)}subscribe(e){return this.subscribers.push(e),e(this.value),()=>{this.subscribers=this.subscribers.filter(t=>t!==e)}}setFetch(e){const t=this.connection;t.setFetch&&t.setFetch(e)}set(e){this.value=e,this.subscribers.forEach(t=>t(e))}async get(e){return await this.hydrate(e),typeof this.value>"u"?null:this.value.filter(s=>s.id==e)[0]}async getWhere(e){return await this.hydrate(null,e),this.value}async getAll(){return await this.hydrate(),this.value}async hydrate(e,t){if(!(this.hydrated&&!t)){if(t){const s=await this.connection.getWhere(t);s&&this.set(s)}else if(e){if(!t){const s=await this.connection.get(e);s&&this.set([s])}}else{const s=await this.connection.getAll();s&&this.set(s)}this.hydrated=!0}}async upsert(e,t=!1){if(Array.isArray(e)){const s=e.map(i=>Object.assign(new this.model,i));t&&s.forEach(i=>this.insertIntoStore(i));const r=await this.connection.upsert(s);return r.forEach(i=>this.insertIntoStore(i)),r}else{const s=Object.assign(new this.model,e);t&&this.insertIntoStore(s);const r=await this.connection.upsert(s);return this.insertIntoStore(r),r}}async remove(e,t=!1){if(Array.isArray(e)){const s=e.map(i=>Object.assign(new this.model,i));t&&s.forEach(i=>this.removeFromStore(i));const r=await this.connection.remove(s);return s.forEach(i=>this.removeFromStore(i)),r}else{const s=Object.assign(new this.model,e);t&&this.removeFromStore(s);const r=await this.connection.remove(s);return this.removeFromStore(s),r}}async trackedUpsert(e){try{const t=await this.upsert(e.model);return this.trackedStatus.push({requestId:e.requestId,error:!1,message:"Upserted successfully"}),t}catch(t){this.trackedStatus.push({requestId:e.requestId,error:!0,message:t.message})}}async trackedRemove(e){try{const t=await this.remove(e.model);return this.trackedStatus.push({requestId:e.requestId,error:!1,message:"Removed successfully"}),t}catch(t){this.trackedStatus.push({requestId:e.requestId,error:!0,message:t.message})}}insertIntoStore(e){this.value.some(s=>s.id===e.id)?this.set(this.value.map(s=>s.id===e.id?e:s)):this.set([...this.value,e])}removeFromStore(e){this.set(this.value.filter(t=>t.id!==e.id))}}exports.AbstractStore=d;exports.DatabaseConnection=c;exports.Hydrator=h;exports.ServerConnection=m;