@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,736 @@
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 - Trash Functionality', () => {
6
+ const mockSchema = MockBookSchema
7
+
8
+ let repository: MockMemoryRepository<typeof mockSchema>
9
+
10
+ beforeEach(() => {
11
+ repository = new MockMemoryRepository({ schema: mockSchema })
12
+ })
13
+
14
+ describe('remove', () => {
15
+ it('should be able to remove an item', async () => {
16
+ // Create a test book
17
+ const book = await repository.create({
18
+ title: 'Book to Remove',
19
+ author: 'Test Author',
20
+ publishedDate: new Date(),
21
+ })
22
+
23
+ // Verify the book exists
24
+ const loadedBook = await repository.load({ id: book.id })
25
+ expect(loadedBook).not.toBeNull()
26
+ expect(loadedBook?.title).toBe('Book to Remove')
27
+
28
+ // Remove the book
29
+ const removedBook = await repository.remove({ id: book.id })
30
+ expect(removedBook.id).toBe(book.id)
31
+
32
+ // Verify the book is no longer in the main data store
33
+ const afterRemove = await repository.load({ id: book.id })
34
+ expect(afterRemove).toBeNull()
35
+ })
36
+
37
+ it('should not be able to load a removed item by default', async () => {
38
+ const book = await repository.create({
39
+ title: 'Book to Remove',
40
+ author: 'Test Author',
41
+ publishedDate: new Date(),
42
+ })
43
+
44
+ await repository.remove({ id: book.id })
45
+
46
+ // Default load should not find removed items
47
+ const loadedBook = await repository.load({ id: book.id })
48
+ expect(loadedBook).toBeNull()
49
+ })
50
+ })
51
+
52
+ describe('load with removedOnly option', () => {
53
+ it('should be able to load a removed item with removedOnly option', async () => {
54
+ const book = await repository.create({
55
+ title: 'Removed Book',
56
+ author: 'Test Author',
57
+ publishedDate: new Date(),
58
+ })
59
+
60
+ await repository.remove({ id: book.id })
61
+
62
+ // Load with removedOnly should find the removed item
63
+ const loadedBook = await repository.load({ id: book.id }, { removedOnly: true })
64
+ expect(loadedBook).not.toBeNull()
65
+ expect(loadedBook?.title).toBe('Removed Book')
66
+ })
67
+
68
+ it('should not load a non-removed item with removedOnly option', async () => {
69
+ const book = await repository.create({
70
+ title: 'Active Book',
71
+ author: 'Test Author',
72
+ publishedDate: new Date(),
73
+ })
74
+
75
+ // Load with removedOnly should not find active items
76
+ const loadedBook = await repository.load({ id: book.id }, { removedOnly: true })
77
+ expect(loadedBook).toBeNull()
78
+ })
79
+ })
80
+
81
+ describe('load with includeRemoved option', () => {
82
+ it('should be able to load a removed item with includeRemoved option', async () => {
83
+ const book = await repository.create({
84
+ title: 'Removed Book',
85
+ author: 'Test Author',
86
+ publishedDate: new Date(),
87
+ })
88
+
89
+ await repository.remove({ id: book.id })
90
+
91
+ // Load with includeRemoved should find the removed item
92
+ const loadedBook = await repository.load({ id: book.id }, { includeRemoved: true })
93
+ expect(loadedBook).not.toBeNull()
94
+ expect(loadedBook?.title).toBe('Removed Book')
95
+ })
96
+
97
+ it('should be able to load a non-removed item with includeRemoved option', async () => {
98
+ const book = await repository.create({
99
+ title: 'Active Book',
100
+ author: 'Test Author',
101
+ publishedDate: new Date(),
102
+ })
103
+
104
+ // Load with includeRemoved should find active items
105
+ const loadedBook = await repository.load({ id: book.id }, { includeRemoved: true })
106
+ expect(loadedBook).not.toBeNull()
107
+ expect(loadedBook?.title).toBe('Active Book')
108
+ })
109
+ })
110
+
111
+ describe('search with removedOnly option', () => {
112
+ it('should be able to search removed items with removedOnly option', async () => {
113
+ const repositoryWithFilter = new MockMemoryRepository({
114
+ schema: mockSchema,
115
+ filter: (data, filters) => {
116
+ if (filters.text) {
117
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
118
+ }
119
+ return true
120
+ },
121
+ })
122
+
123
+ // Create test data
124
+ await repositoryWithFilter.create({ title: 'Active Book 1', author: 'Author 1', publishedDate: new Date() })
125
+ const removedBook1 = await repositoryWithFilter.create({
126
+ title: 'Removed Book 1',
127
+ author: 'Author 2',
128
+ publishedDate: new Date(),
129
+ })
130
+ const removedBook2 = await repositoryWithFilter.create({
131
+ title: 'Removed Book 2',
132
+ author: 'Author 3',
133
+ publishedDate: new Date(),
134
+ })
135
+
136
+ // Remove two books
137
+ await repositoryWithFilter.remove({ id: removedBook1.id })
138
+ await repositoryWithFilter.remove({ id: removedBook2.id })
139
+
140
+ // Search with removedOnly should only find removed items
141
+ const results = await repositoryWithFilter.search({}, { removedOnly: true })
142
+ expect(results.results).toHaveLength(2)
143
+ expect(results.results.every((book) => book.title.startsWith('Removed'))).toBe(true)
144
+ })
145
+
146
+ it('should be able to filter removed items with removedOnly option', async () => {
147
+ const repositoryWithFilter = new MockMemoryRepository({
148
+ schema: mockSchema,
149
+ filter: (data, filters) => {
150
+ if (filters.text) {
151
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
152
+ }
153
+ return true
154
+ },
155
+ })
156
+
157
+ // Create test data
158
+ const removedBook1 = await repositoryWithFilter.create({
159
+ title: 'Test Removed Book',
160
+ author: 'Author 1',
161
+ publishedDate: new Date(),
162
+ })
163
+ const removedBook2 = await repositoryWithFilter.create({
164
+ title: 'Other Removed Book',
165
+ author: 'Author 2',
166
+ publishedDate: new Date(),
167
+ })
168
+
169
+ await repositoryWithFilter.remove({ id: removedBook1.id })
170
+ await repositoryWithFilter.remove({ id: removedBook2.id })
171
+
172
+ // Search with removedOnly and filter
173
+ const results = await repositoryWithFilter.search({ text: 'Test' }, { removedOnly: true })
174
+ expect(results.results).toHaveLength(1)
175
+ expect(results.results[0].title).toBe('Test Removed Book')
176
+ })
177
+
178
+ it('should not return non-removed items with removedOnly option', async () => {
179
+ const repositoryWithFilter = new MockMemoryRepository({
180
+ schema: mockSchema,
181
+ filter: (data, filters) => {
182
+ if (filters.text) {
183
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
184
+ }
185
+ return true
186
+ },
187
+ })
188
+
189
+ // Create active items only
190
+ await repositoryWithFilter.create({ title: 'Active Book 1', author: 'Author 1', publishedDate: new Date() })
191
+ await repositoryWithFilter.create({ title: 'Active Book 2', author: 'Author 2', publishedDate: new Date() })
192
+
193
+ // Search with removedOnly should return empty results
194
+ const results = await repositoryWithFilter.search({}, { removedOnly: true })
195
+ expect(results.results).toHaveLength(0)
196
+ })
197
+ })
198
+
199
+ describe('search with includeRemoved option', () => {
200
+ it('should be able to search both removed and non-removed items with includeRemoved option', async () => {
201
+ const repositoryWithFilter = new MockMemoryRepository({
202
+ schema: mockSchema,
203
+ filter: (data, filters) => {
204
+ if (filters.text) {
205
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
206
+ }
207
+ return true
208
+ },
209
+ })
210
+
211
+ // Create test data
212
+ await repositoryWithFilter.create({ title: 'Active Book 1', author: 'Author 1', publishedDate: new Date() })
213
+ await repositoryWithFilter.create({ title: 'Active Book 2', author: 'Author 2', publishedDate: new Date() })
214
+ const removedBook = await repositoryWithFilter.create({
215
+ title: 'Removed Book',
216
+ author: 'Author 3',
217
+ publishedDate: new Date(),
218
+ })
219
+
220
+ await repositoryWithFilter.remove({ id: removedBook.id })
221
+
222
+ // Search with includeRemoved should find all items
223
+ const results = await repositoryWithFilter.search({}, { includeRemoved: true })
224
+ expect(results.results).toHaveLength(3)
225
+ })
226
+
227
+ it('should be able to filter across removed and non-removed items with includeRemoved option', async () => {
228
+ const repositoryWithFilter = new MockMemoryRepository({
229
+ schema: mockSchema,
230
+ filter: (data, filters) => {
231
+ if (filters.text) {
232
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
233
+ }
234
+ return true
235
+ },
236
+ })
237
+
238
+ // Create test data
239
+ await repositoryWithFilter.create({
240
+ title: 'Test Active Book',
241
+ author: 'Author 1',
242
+ publishedDate: new Date(),
243
+ })
244
+ const removedBook = await repositoryWithFilter.create({
245
+ title: 'Test Removed Book',
246
+ author: 'Author 2',
247
+ publishedDate: new Date(),
248
+ })
249
+ await repositoryWithFilter.create({ title: 'Other Book', author: 'Author 3', publishedDate: new Date() })
250
+ const otherRemovedBook = await repositoryWithFilter.create({
251
+ title: 'Another Removed Book',
252
+ author: 'Author 4',
253
+ publishedDate: new Date(),
254
+ })
255
+
256
+ await repositoryWithFilter.remove({ id: removedBook.id })
257
+ await repositoryWithFilter.remove({ id: otherRemovedBook.id })
258
+
259
+ // Search with includeRemoved and filter
260
+ const results = await repositoryWithFilter.search({ text: 'Test' }, { includeRemoved: true })
261
+ expect(results.results).toHaveLength(2)
262
+ expect(results.results.some((book) => book.title === 'Test Active Book')).toBe(true)
263
+ expect(results.results.some((book) => book.title === 'Test Removed Book')).toBe(true)
264
+ })
265
+ })
266
+
267
+ describe('permanentlyDeleteFromTrash', () => {
268
+ it('should be able to permanently delete a previously removed item', async () => {
269
+ const book = await repository.create({
270
+ title: 'Book to Permanently Delete',
271
+ author: 'Test Author',
272
+ publishedDate: new Date(),
273
+ })
274
+
275
+ // Remove the book
276
+ await repository.remove({ id: book.id })
277
+
278
+ // Verify it's in trash
279
+ const inTrash = await repository.load({ id: book.id }, { removedOnly: true })
280
+ expect(inTrash).not.toBeNull()
281
+
282
+ // Permanently delete from trash
283
+ const deleted = await repository.permanentlyDeleteFromTrash({ id: book.id })
284
+ expect(deleted.id).toBe(book.id)
285
+
286
+ // Verify it's no longer in trash
287
+ const afterDelete = await repository.load({ id: book.id }, { removedOnly: true })
288
+ expect(afterDelete).toBeNull()
289
+
290
+ // Verify it's not in main data either
291
+ const inMain = await repository.load({ id: book.id })
292
+ expect(inMain).toBeNull()
293
+ })
294
+
295
+ it('should throw error when trying to permanently delete non-existent item from trash', async () => {
296
+ await expect(repository.permanentlyDeleteFromTrash({ id: 999 })).rejects.toThrow()
297
+ })
298
+
299
+ it('should throw error when trying to permanently delete a non-removed item from trash', async () => {
300
+ const book = await repository.create({
301
+ title: 'Active Book',
302
+ author: 'Test Author',
303
+ publishedDate: new Date(),
304
+ })
305
+
306
+ // Trying to permanently delete from trash without removing first should fail
307
+ await expect(repository.permanentlyDeleteFromTrash({ id: book.id })).rejects.toThrow()
308
+ })
309
+ })
310
+
311
+ describe('permanentlyDelete', () => {
312
+ it('should be able to permanently delete a previously removed item', async () => {
313
+ const book = await repository.create({
314
+ title: 'Book to Permanently Delete',
315
+ author: 'Test Author',
316
+ publishedDate: new Date(),
317
+ })
318
+
319
+ // Remove the book
320
+ await repository.remove({ id: book.id })
321
+
322
+ // Verify it's in trash
323
+ const inTrash = await repository.load({ id: book.id }, { removedOnly: true })
324
+ expect(inTrash).not.toBeNull()
325
+
326
+ // Permanently delete
327
+ const deleted = await repository.permanentlyDelete({ id: book.id })
328
+ expect(deleted.id).toBe(book.id)
329
+
330
+ // Verify it's no longer anywhere
331
+ const afterDelete = await repository.load({ id: book.id }, { removedOnly: true })
332
+ expect(afterDelete).toBeNull()
333
+ })
334
+
335
+ it('should be able to permanently delete a non-removed item', async () => {
336
+ const book = await repository.create({
337
+ title: 'Active Book to Delete',
338
+ author: 'Test Author',
339
+ publishedDate: new Date(),
340
+ })
341
+
342
+ // Verify it exists
343
+ const exists = await repository.load({ id: book.id })
344
+ expect(exists).not.toBeNull()
345
+
346
+ // Permanently delete without removing first
347
+ const deleted = await repository.permanentlyDelete({ id: book.id })
348
+ expect(deleted.id).toBe(book.id)
349
+
350
+ // Verify it's no longer in main data
351
+ const afterDelete = await repository.load({ id: book.id })
352
+ expect(afterDelete).toBeNull()
353
+
354
+ // Verify it's not in trash either
355
+ const inTrash = await repository.load({ id: book.id }, { removedOnly: true })
356
+ expect(inTrash).toBeNull()
357
+ })
358
+
359
+ it('should throw error when trying to permanently delete non-existent item', async () => {
360
+ await expect(repository.permanentlyDelete({ id: 999 })).rejects.toThrow()
361
+ })
362
+ })
363
+
364
+ describe('emptyTrash', () => {
365
+ it('should permanently delete all items from trash when no filter is provided', async () => {
366
+ // Create and remove multiple items
367
+ const book1 = await repository.create({ title: 'Book 1', author: 'Author 1', publishedDate: new Date() })
368
+ const book2 = await repository.create({ title: 'Book 2', author: 'Author 2', publishedDate: new Date() })
369
+ const book3 = await repository.create({ title: 'Book 3', author: 'Author 3', publishedDate: new Date() })
370
+
371
+ await repository.remove({ id: book1.id })
372
+ await repository.remove({ id: book2.id })
373
+ await repository.remove({ id: book3.id })
374
+
375
+ // Verify all are in trash
376
+ const trashResults = await repository.search({}, { removedOnly: true })
377
+ expect(trashResults.results).toHaveLength(3)
378
+
379
+ // Empty trash
380
+ const deletedCount = await repository.emptyTrash()
381
+ expect(deletedCount).toBe(3)
382
+
383
+ // Verify trash is empty
384
+ const afterEmpty = await repository.search({}, { removedOnly: true })
385
+ expect(afterEmpty.results).toHaveLength(0)
386
+ })
387
+
388
+ it('should permanently delete filtered items from trash when filter is provided', async () => {
389
+ const repositoryWithFilter = new MockMemoryRepository({
390
+ schema: mockSchema,
391
+ filter: (data, filters) => {
392
+ if (filters.text) {
393
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
394
+ }
395
+ return true
396
+ },
397
+ })
398
+
399
+ // Create and remove multiple items
400
+ const book1 = await repositoryWithFilter.create({
401
+ title: 'Test Book 1',
402
+ author: 'Author 1',
403
+ publishedDate: new Date(),
404
+ })
405
+ const book2 = await repositoryWithFilter.create({
406
+ title: 'Test Book 2',
407
+ author: 'Author 2',
408
+ publishedDate: new Date(),
409
+ })
410
+ const book3 = await repositoryWithFilter.create({
411
+ title: 'Other Book',
412
+ author: 'Author 3',
413
+ publishedDate: new Date(),
414
+ })
415
+
416
+ await repositoryWithFilter.remove({ id: book1.id })
417
+ await repositoryWithFilter.remove({ id: book2.id })
418
+ await repositoryWithFilter.remove({ id: book3.id })
419
+
420
+ // Empty trash with filter
421
+ const deletedCount = await repositoryWithFilter.emptyTrash({ text: 'Test' })
422
+ expect(deletedCount).toBe(2)
423
+
424
+ // Verify only filtered items were deleted
425
+ const trashResults = await repositoryWithFilter.search({}, { removedOnly: true })
426
+ expect(trashResults.results).toHaveLength(1)
427
+ expect(trashResults.results[0].title).toBe('Other Book')
428
+ })
429
+
430
+ it('should return 0 when trash is already empty', async () => {
431
+ const deletedCount = await repository.emptyTrash()
432
+ expect(deletedCount).toBe(0)
433
+ })
434
+
435
+ it('should not affect non-removed items', async () => {
436
+ // Create items
437
+ await repository.create({ title: 'Active Book 1', author: 'Author 1', publishedDate: new Date() })
438
+ await repository.create({ title: 'Active Book 2', author: 'Author 2', publishedDate: new Date() })
439
+
440
+ const book3 = await repository.create({
441
+ title: 'Book to Remove',
442
+ author: 'Author 3',
443
+ publishedDate: new Date(),
444
+ })
445
+ await repository.remove({ id: book3.id })
446
+
447
+ // Empty trash
448
+ await repository.emptyTrash()
449
+
450
+ // Verify active items are still there
451
+ const activeResults = await repository.search({})
452
+ expect(activeResults.results).toHaveLength(2)
453
+ })
454
+
455
+ it('when no filter function is configured, remove all items from trash', async () => {
456
+ // Create and remove items
457
+ const book1 = await repository.create({
458
+ title: 'Test Book 1',
459
+ author: 'Author 1',
460
+ publishedDate: new Date(),
461
+ })
462
+ const book2 = await repository.create({
463
+ title: 'Other Book 2',
464
+ author: 'Author 2',
465
+ publishedDate: new Date(),
466
+ })
467
+
468
+ await repository.remove({ id: book1.id })
469
+ await repository.remove({ id: book2.id })
470
+
471
+ // Empty trash with filter
472
+ // Since no filter function is provided, all items in trash should be deleted
473
+ const deletedCount = await repository.emptyTrash({ text: 'Test' })
474
+ expect(deletedCount).toBe(2)
475
+
476
+ // Verify all items are still in trash
477
+ const trashResults = await repository.search({}, { removedOnly: true })
478
+ expect(trashResults.results).toHaveLength(0)
479
+ })
480
+ })
481
+
482
+ describe('restore', () => {
483
+ it('should restore a removed item back to active state', async () => {
484
+ const book = await repository.create({
485
+ title: 'Book to Restore',
486
+ author: 'Test Author',
487
+ publishedDate: new Date(),
488
+ })
489
+
490
+ // Remove the book
491
+ await repository.remove({ id: book.id })
492
+
493
+ // Verify it's in trash
494
+ const inTrash = await repository.load({ id: book.id }, { removedOnly: true })
495
+ expect(inTrash).not.toBeNull()
496
+
497
+ // Restore the book
498
+ const restored = await repository.restore({ id: book.id })
499
+ expect(restored.id).toBe(book.id)
500
+
501
+ // Verify it's back in main data
502
+ const restoredBook = await repository.load({ id: book.id })
503
+ expect(restoredBook).not.toBeNull()
504
+ expect(restoredBook?.title).toBe('Book to Restore')
505
+
506
+ // Verify it's no longer in trash
507
+ const notInTrash = await repository.load({ id: book.id }, { removedOnly: true })
508
+ expect(notInTrash).toBeNull()
509
+ })
510
+
511
+ it('should throw error when trying to restore non-existent item', async () => {
512
+ await expect(repository.restore({ id: 999 })).rejects.toThrow('Item not found in trash')
513
+ })
514
+
515
+ it('should throw error when trying to restore a non-removed item', async () => {
516
+ const book = await repository.create({
517
+ title: 'Active Book',
518
+ author: 'Test Author',
519
+ publishedDate: new Date(),
520
+ })
521
+
522
+ // Trying to restore an active item should fail
523
+ await expect(repository.restore({ id: book.id })).rejects.toThrow('Item not found in trash')
524
+ })
525
+ })
526
+
527
+ describe('count with trash options', () => {
528
+ it('should count only active items by default', async () => {
529
+ await repository.create({
530
+ title: 'Active Book',
531
+ author: 'Author 1',
532
+ publishedDate: new Date(),
533
+ })
534
+ const book2 = await repository.create({
535
+ title: 'Book to Remove',
536
+ author: 'Author 2',
537
+ publishedDate: new Date(),
538
+ })
539
+
540
+ await repository.remove({ id: book2.id })
541
+
542
+ const count = await repository.count({})
543
+ expect(count).toBe(1)
544
+ })
545
+
546
+ it('should count only removed items with removedOnly option', async () => {
547
+ await repository.create({
548
+ title: 'Active Book',
549
+ author: 'Author 1',
550
+ publishedDate: new Date(),
551
+ })
552
+ const book2 = await repository.create({
553
+ title: 'Book to Remove 1',
554
+ author: 'Author 2',
555
+ publishedDate: new Date(),
556
+ })
557
+ const book3 = await repository.create({
558
+ title: 'Book to Remove 2',
559
+ author: 'Author 3',
560
+ publishedDate: new Date(),
561
+ })
562
+
563
+ await repository.remove({ id: book2.id })
564
+ await repository.remove({ id: book3.id })
565
+
566
+ const count = await repository.count({}, { removedOnly: true })
567
+ expect(count).toBe(2)
568
+ })
569
+
570
+ it('should count both removed and active items with includeRemoved option', async () => {
571
+ await repository.create({
572
+ title: 'Active Book',
573
+ author: 'Author 1',
574
+ publishedDate: new Date(),
575
+ })
576
+ const book2 = await repository.create({
577
+ title: 'Book to Remove',
578
+ author: 'Author 2',
579
+ publishedDate: new Date(),
580
+ })
581
+
582
+ await repository.remove({ id: book2.id })
583
+
584
+ const count = await repository.count({}, { includeRemoved: true })
585
+ expect(count).toBe(2)
586
+ })
587
+
588
+ it('should count filtered active items by default', async () => {
589
+ const repositoryWithFilter = new MockMemoryRepository({
590
+ schema: mockSchema,
591
+ filter: (data, filters) => {
592
+ if (filters.text) {
593
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
594
+ }
595
+ return true
596
+ },
597
+ })
598
+
599
+ await repositoryWithFilter.create({ title: 'Test Book 1', author: 'Author 1', publishedDate: new Date() })
600
+ await repositoryWithFilter.create({ title: 'Test Book 2', author: 'Author 2', publishedDate: new Date() })
601
+ await repositoryWithFilter.create({ title: 'Other Book', author: 'Author 3', publishedDate: new Date() })
602
+ const removedBook = await repositoryWithFilter.create({
603
+ title: 'Test Book 3',
604
+ author: 'Author 4',
605
+ publishedDate: new Date(),
606
+ })
607
+
608
+ await repositoryWithFilter.remove({ id: removedBook.id })
609
+
610
+ // Count with filter should only count active items matching filter
611
+ const count = await repositoryWithFilter.count({ text: 'Test' })
612
+ expect(count).toBe(2)
613
+ })
614
+
615
+ it('should count filtered removed items with removedOnly option', async () => {
616
+ const repositoryWithFilter = new MockMemoryRepository({
617
+ schema: mockSchema,
618
+ filter: (data, filters) => {
619
+ if (filters.text) {
620
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
621
+ }
622
+ return true
623
+ },
624
+ })
625
+
626
+ const book1 = await repositoryWithFilter.create({
627
+ title: 'Test Removed Book 1',
628
+ author: 'Author 1',
629
+ publishedDate: new Date(),
630
+ })
631
+ const book2 = await repositoryWithFilter.create({
632
+ title: 'Test Removed Book 2',
633
+ author: 'Author 2',
634
+ publishedDate: new Date(),
635
+ })
636
+ const book3 = await repositoryWithFilter.create({
637
+ title: 'Other Removed Book',
638
+ author: 'Author 3',
639
+ publishedDate: new Date(),
640
+ })
641
+ await repositoryWithFilter.create({ title: 'Active Book', author: 'Author 4', publishedDate: new Date() })
642
+
643
+ await repositoryWithFilter.remove({ id: book1.id })
644
+ await repositoryWithFilter.remove({ id: book2.id })
645
+ await repositoryWithFilter.remove({ id: book3.id })
646
+
647
+ // Count with filter and removedOnly should only count removed items matching filter
648
+ const count = await repositoryWithFilter.count({ text: 'Test' }, { removedOnly: true })
649
+ expect(count).toBe(2)
650
+ })
651
+
652
+ it('should count filtered items across active and removed with includeRemoved option', async () => {
653
+ const repositoryWithFilter = new MockMemoryRepository({
654
+ schema: mockSchema,
655
+ filter: (data, filters) => {
656
+ if (filters.text) {
657
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
658
+ }
659
+ return true
660
+ },
661
+ })
662
+
663
+ await repositoryWithFilter.create({
664
+ title: 'Test Active Book 1',
665
+ author: 'Author 1',
666
+ publishedDate: new Date(),
667
+ })
668
+ await repositoryWithFilter.create({
669
+ title: 'Test Active Book 2',
670
+ author: 'Author 2',
671
+ publishedDate: new Date(),
672
+ })
673
+ const removedBook1 = await repositoryWithFilter.create({
674
+ title: 'Test Removed Book 1',
675
+ author: 'Author 3',
676
+ publishedDate: new Date(),
677
+ })
678
+ const removedBook2 = await repositoryWithFilter.create({
679
+ title: 'Test Removed Book 2',
680
+ author: 'Author 4',
681
+ publishedDate: new Date(),
682
+ })
683
+ await repositoryWithFilter.create({
684
+ title: 'Other Active Book',
685
+ author: 'Author 5',
686
+ publishedDate: new Date(),
687
+ })
688
+
689
+ await repositoryWithFilter.remove({ id: removedBook1.id })
690
+ await repositoryWithFilter.remove({ id: removedBook2.id })
691
+
692
+ // Count with filter and includeRemoved should count all items matching filter
693
+ const count = await repositoryWithFilter.count({ text: 'Test' }, { includeRemoved: true })
694
+ expect(count).toBe(4)
695
+ })
696
+
697
+ it('should return 0 when no items match filter in active items', async () => {
698
+ const repositoryWithFilter = new MockMemoryRepository({
699
+ schema: mockSchema,
700
+ filter: (data, filters) => {
701
+ if (filters.text) {
702
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
703
+ }
704
+ return true
705
+ },
706
+ })
707
+
708
+ await repositoryWithFilter.create({ title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
709
+
710
+ const count = await repositoryWithFilter.count({ text: 'NonExistent' })
711
+ expect(count).toBe(0)
712
+ })
713
+
714
+ it('should return 0 when no removed items match filter with removedOnly option', async () => {
715
+ const repositoryWithFilter = new MockMemoryRepository({
716
+ schema: mockSchema,
717
+ filter: (data, filters) => {
718
+ if (filters.text) {
719
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
720
+ }
721
+ return true
722
+ },
723
+ })
724
+
725
+ const book = await repositoryWithFilter.create({
726
+ title: 'Removed Book',
727
+ author: 'Author 1',
728
+ publishedDate: new Date(),
729
+ })
730
+ await repositoryWithFilter.remove({ id: book.id })
731
+
732
+ const count = await repositoryWithFilter.count({ text: 'NonExistent' }, { removedOnly: true })
733
+ expect(count).toBe(0)
734
+ })
735
+ })
736
+ })