@declaro/data 2.0.0-beta.120 → 2.0.0-beta.125

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 (47) hide show
  1. package/dist/browser/index.js +14 -14
  2. package/dist/browser/index.js.map +11 -11
  3. package/dist/node/index.cjs +163 -45
  4. package/dist/node/index.cjs.map +11 -11
  5. package/dist/node/index.js +163 -45
  6. package/dist/node/index.js.map +11 -11
  7. package/dist/ts/application/model-controller.d.ts +22 -1
  8. package/dist/ts/application/model-controller.d.ts.map +1 -1
  9. package/dist/ts/domain/events/domain-event.d.ts +1 -1
  10. package/dist/ts/domain/events/domain-event.d.ts.map +1 -1
  11. package/dist/ts/domain/events/event-types.d.ts +10 -1
  12. package/dist/ts/domain/events/event-types.d.ts.map +1 -1
  13. package/dist/ts/domain/events/mutation-event.d.ts +5 -2
  14. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -1
  15. package/dist/ts/domain/events/query-event.d.ts +4 -2
  16. package/dist/ts/domain/events/query-event.d.ts.map +1 -1
  17. package/dist/ts/domain/events/request-event.d.ts +17 -2
  18. package/dist/ts/domain/events/request-event.d.ts.map +1 -1
  19. package/dist/ts/domain/interfaces/repository.d.ts +26 -0
  20. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -1
  21. package/dist/ts/domain/services/model-service.d.ts +19 -1
  22. package/dist/ts/domain/services/model-service.d.ts.map +1 -1
  23. package/dist/ts/domain/services/read-only-model-service.d.ts +19 -0
  24. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -1
  25. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +21 -3
  26. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -1
  27. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  28. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  29. package/package.json +5 -5
  30. package/src/application/model-controller.test.ts +191 -0
  31. package/src/application/model-controller.ts +44 -1
  32. package/src/domain/events/domain-event.ts +1 -1
  33. package/src/domain/events/event-types.ts +9 -0
  34. package/src/domain/events/mutation-event.test.ts +369 -17
  35. package/src/domain/events/mutation-event.ts +10 -2
  36. package/src/domain/events/query-event.test.ts +218 -18
  37. package/src/domain/events/query-event.ts +8 -2
  38. package/src/domain/events/request-event.test.ts +1 -1
  39. package/src/domain/events/request-event.ts +22 -7
  40. package/src/domain/interfaces/repository.ts +29 -0
  41. package/src/domain/services/model-service.normalization.test.ts +6 -6
  42. package/src/domain/services/model-service.test.ts +311 -7
  43. package/src/domain/services/model-service.ts +88 -1
  44. package/src/domain/services/read-only-model-service.test.ts +396 -0
  45. package/src/domain/services/read-only-model-service.ts +23 -3
  46. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  47. package/src/test/mock/repositories/mock-memory-repository.ts +146 -46
@@ -1,7 +1,7 @@
1
1
  import type { AuthValidator } from '@declaro/auth'
2
2
  import { PermissionValidator, type AnyModelSchema } from '@declaro/core'
3
3
  import type { ModelService, ICreateOptions, IUpdateOptions } from '../domain/services/model-service'
4
- import type { InferDetail, InferInput, InferLookup, InferSummary } from '../shared/utils/schema-inference'
4
+ import type { InferDetail, InferFilters, InferInput, InferLookup, InferSummary } from '../shared/utils/schema-inference'
5
5
  import { ReadOnlyModelController } from './read-only-model-controller'
6
6
 
7
7
  export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyModelController<TSchema> {
@@ -89,4 +89,47 @@ export class ModelController<TSchema extends AnyModelSchema> extends ReadOnlyMod
89
89
  )
90
90
  return this.service.bulkUpsert(inputs, options)
91
91
  }
92
+
93
+ /**
94
+ * Permanently deletes a specific entity from the trash.
95
+ * Requires 'permanently-delete-from-trash', 'permanently-delete', or 'empty-trash' permission.
96
+ * @param lookup The lookup object containing entity identifiers
97
+ * @returns The permanently deleted entity summary
98
+ */
99
+ async permanentlyDeleteFromTrash(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
100
+ this.authValidator.validatePermissions((v) =>
101
+ v.someOf([
102
+ this.service.getDescriptor('permanently-delete-from-trash', '*').toString(),
103
+ this.service.getDescriptor('permanently-delete', '*').toString(),
104
+ this.service.getDescriptor('empty-trash', '*').toString(),
105
+ ]),
106
+ )
107
+ return this.service.permanentlyDeleteFromTrash(lookup)
108
+ }
109
+
110
+ /**
111
+ * Permanently deletes an entity without moving it to trash first.
112
+ * Requires 'permanently-delete' permission.
113
+ * @param lookup The lookup object containing entity identifiers
114
+ * @returns The permanently deleted entity summary
115
+ */
116
+ async permanentlyDelete(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>> {
117
+ this.authValidator.validatePermissions((v) =>
118
+ v.someOf([this.service.getDescriptor('permanently-delete', '*').toString()]),
119
+ )
120
+ return this.service.permanentlyDelete(lookup)
121
+ }
122
+
123
+ /**
124
+ * Empties the trash by permanently deleting entities that have been marked as removed.
125
+ * Requires 'empty-trash' permission.
126
+ * @param filters Optional filters to apply when selecting entities to delete
127
+ * @returns The count of entities permanently deleted
128
+ */
129
+ async emptyTrash(filters?: InferFilters<TSchema>): Promise<number> {
130
+ this.authValidator.validatePermissions((v) =>
131
+ v.someOf([this.service.getDescriptor('empty-trash', '*').toString()]),
132
+ )
133
+ return this.service.emptyTrash(filters)
134
+ }
92
135
  }
@@ -24,7 +24,7 @@ export interface IDomainEventOptions<TData, TMeta = any> {
24
24
  export interface IDomainEventJSON<T, M = any> {
25
25
  eventId: string
26
26
  data?: T
27
- meta?: M
27
+ meta: M
28
28
  timestamp: string // JSON-compatible format
29
29
  type: string
30
30
  session?: { id: string } // Simplified session representation
@@ -22,4 +22,13 @@ export enum ModelMutationAction {
22
22
  Restore = 'restore',
23
23
  BeforeRestore = 'beforeRestore',
24
24
  AfterRestore = 'afterRestore',
25
+ EmptyTrash = 'emptyTrash',
26
+ BeforeEmptyTrash = 'beforeEmptyTrash',
27
+ AfterEmptyTrash = 'afterEmptyTrash',
28
+ PermanentlyDeleteFromTrash = 'permanentlyDeleteFromTrash',
29
+ BeforePermanentlyDeleteFromTrash = 'beforePermanentlyDeleteFromTrash',
30
+ AfterPermanentlyDeleteFromTrash = 'afterPermanentlyDeleteFromTrash',
31
+ PermanentlyDelete = 'permanentlyDelete',
32
+ BeforePermanentlyDelete = 'beforePermanentlyDelete',
33
+ AfterPermanentlyDelete = 'afterPermanentlyDelete',
25
34
  }
@@ -1,38 +1,390 @@
1
1
  import { describe, it, expect } from 'bun:test'
2
2
  import { MutationEvent } from './mutation-event'
3
+ import { ActionDescriptor, type IActionDescriptorInput } from '@declaro/core'
4
+
5
+ interface IBookResult {
6
+ id: string
7
+ title: string
8
+ description: string
9
+ author: string
10
+ year: number
11
+ }
12
+
13
+ interface IBookInput {
14
+ title: string
15
+ description: string
16
+ author: string
17
+ year: number
18
+ }
3
19
 
4
20
  describe('MutationEvent', () => {
5
21
  it('should create a mutation event with input and descriptor', () => {
6
- const input = { key: 'value' }
7
- const descriptor = { namespace: 'test', action: 'create' }
8
- const event = new MutationEvent(descriptor, input, {})
22
+ const input: IBookInput = {
23
+ title: 'Test Book',
24
+ description: 'A book for testing',
25
+ author: 'Author Name',
26
+ year: 2024,
27
+ }
28
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
29
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
9
30
 
10
- expect(event.descriptor.namespace).toBe('test')
11
- expect(event.descriptor.action).toBe('create')
12
- expect(event.meta.input).toEqual(input)
31
+ expect(event.input).toEqual(input)
32
+ expect(event.descriptor.toString()).toEqual(new ActionDescriptor(descriptor).toString())
33
+ expect(event.meta).toEqual({})
34
+ expect(event.data).toBeUndefined()
13
35
  })
14
36
 
15
- it('should update meta correctly', () => {
16
- const input = { key: 'value' }
17
- const descriptor = { namespace: 'test', action: 'create' }
18
- const event = new MutationEvent(descriptor, input, {
19
- foo: 'bar',
37
+ it('should allow setting existing result in meta', () => {
38
+ const input: IBookInput = {
39
+ title: 'Test Book',
40
+ description: 'A book for testing',
41
+ author: 'Author Name',
42
+ year: 2024,
43
+ }
44
+ const existing: IBookResult = {
45
+ id: '1',
46
+ title: 'Old Title',
47
+ description: 'Old Description',
48
+ author: 'Old Author',
49
+ year: 2000,
50
+ }
51
+
52
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'update' }
53
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input, {
54
+ existing,
20
55
  })
21
56
 
57
+ expect(event.input).toEqual(input)
58
+ expect(event.meta.existing).toEqual(existing)
59
+ })
60
+
61
+ it('should update meta correctly', () => {
62
+ const input: IBookInput = {
63
+ title: 'Test Book',
64
+ description: 'A book for testing',
65
+ author: 'Author Name',
66
+ year: 2024,
67
+ }
68
+
69
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
70
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
71
+
72
+ const existing: IBookResult = {
73
+ id: '1',
74
+ title: 'Old Title',
75
+ description: 'Old Description',
76
+ author: 'Old Author',
77
+ year: 2000,
78
+ }
79
+
22
80
  event.setMeta({
23
- foo: 'baz',
81
+ existing,
24
82
  })
25
83
 
26
- expect(event.meta.foo).toBe('baz')
84
+ expect(event.meta.existing).toEqual(existing)
27
85
  })
28
86
 
29
87
  it('should set result correctly', () => {
30
- const input = { key: 'value' }
31
- const descriptor = { namespace: 'test', action: 'create' }
32
- const event = new MutationEvent(descriptor, input, {})
88
+ const input: IBookInput = {
89
+ title: 'Test Book',
90
+ description: 'A book for testing',
91
+ author: 'Author Name',
92
+ year: 2024,
93
+ }
94
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
95
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
96
+
97
+ expect(event.data).toBeUndefined()
98
+
99
+ const result: IBookResult = {
100
+ id: '1',
101
+ title: 'Test Book',
102
+ description: 'A book for testing',
103
+ author: 'Author Name',
104
+ year: 2024,
105
+ }
106
+ event.setResult(result)
107
+
108
+ expect(event.data).toEqual(result)
109
+ })
110
+
111
+ it('should serialize to JSON with eventId and timestamp', () => {
112
+ const input: IBookInput = {
113
+ title: 'Test Book',
114
+ description: 'A book for testing',
115
+ author: 'Author Name',
116
+ year: 2024,
117
+ }
118
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
119
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
120
+
121
+ const result: IBookResult = {
122
+ id: '1',
123
+ title: 'Test Book',
124
+ description: 'A book for testing',
125
+ author: 'Author Name',
126
+ year: 2024,
127
+ }
128
+ event.setResult(result)
129
+
130
+ const json = event.toJSON()
131
+
132
+ expect(json.eventId).toBeDefined()
133
+ expect(json.timestamp).toBeDefined()
134
+ expect(json.type).toBe('books::book.create')
135
+ expect(json.input).toEqual(input)
136
+ expect(json.data).toEqual(result)
137
+ })
138
+
139
+ it('should handle chaining of setter methods', () => {
140
+ const input: IBookInput = {
141
+ title: 'Test Book',
142
+ description: 'A book for testing',
143
+ author: 'Author Name',
144
+ year: 2024,
145
+ }
146
+ const existing: IBookResult = {
147
+ id: '1',
148
+ title: 'Old Title',
149
+ description: 'Old Description',
150
+ author: 'Old Author',
151
+ year: 2000,
152
+ }
153
+ const result: IBookResult = {
154
+ id: '1',
155
+ title: 'Test Book',
156
+ description: 'A book for testing',
157
+ author: 'Author Name',
158
+ year: 2024,
159
+ }
160
+
161
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'update' }
162
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
163
+ .setMeta({ existing })
164
+ .setResult(result)
165
+
166
+ expect(event.input).toEqual(input)
167
+ expect(event.meta.existing).toEqual(existing)
168
+ expect(event.data).toEqual(result)
169
+ })
170
+
171
+ it('should update input after initialization', () => {
172
+ const input: IBookInput = {
173
+ title: 'Test Book',
174
+ description: 'A book for testing',
175
+ author: 'Author Name',
176
+ year: 2024,
177
+ }
178
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
179
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
180
+
181
+ const newInput: IBookInput = {
182
+ title: 'Updated Book',
183
+ description: 'Updated description',
184
+ author: 'New Author',
185
+ year: 2025,
186
+ }
187
+ event.setInput(newInput)
188
+
189
+ expect(event.input).toEqual(newInput)
190
+ })
191
+
192
+ it('should allow setting input multiple times', () => {
193
+ const input: IBookInput = {
194
+ title: 'First Book',
195
+ description: 'First description',
196
+ author: 'First Author',
197
+ year: 2024,
198
+ }
199
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
200
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
201
+
202
+ const secondInput: IBookInput = {
203
+ title: 'Second Book',
204
+ description: 'Second description',
205
+ author: 'Second Author',
206
+ year: 2025,
207
+ }
208
+ event.setInput(secondInput)
209
+ expect(event.input).toEqual(secondInput)
33
210
 
34
- const result = { success: true }
211
+ const thirdInput: IBookInput = {
212
+ title: 'Third Book',
213
+ description: 'Third description',
214
+ author: 'Third Author',
215
+ year: 2026,
216
+ }
217
+ event.setInput(thirdInput)
218
+ expect(event.input).toEqual(thirdInput)
219
+ })
220
+
221
+ it('should return this from setInput for chaining', () => {
222
+ const input: IBookInput = {
223
+ title: 'Test Book',
224
+ description: 'A book for testing',
225
+ author: 'Author Name',
226
+ year: 2024,
227
+ }
228
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
229
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
230
+
231
+ const newInput: IBookInput = {
232
+ title: 'Updated Book',
233
+ description: 'Updated description',
234
+ author: 'New Author',
235
+ year: 2025,
236
+ }
237
+ const result: IBookResult = {
238
+ id: '1',
239
+ ...newInput,
240
+ }
241
+
242
+ const returned = event.setInput(newInput).setResult(result)
243
+
244
+ expect(returned).toBe(event)
245
+ expect(event.input).toEqual(newInput)
246
+ expect(event.data).toEqual(result)
247
+ })
248
+
249
+ it('should replace entire input object when using setInput', () => {
250
+ const input: IBookInput = {
251
+ title: 'Original Book',
252
+ description: 'Original description',
253
+ author: 'Original Author',
254
+ year: 2024,
255
+ }
256
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'update' }
257
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
258
+
259
+ expect(event.input.title).toBe('Original Book')
260
+
261
+ const partialInput: IBookInput = {
262
+ title: 'New Title',
263
+ description: 'New description',
264
+ author: 'New Author',
265
+ year: 2025,
266
+ }
267
+ event.setInput(partialInput)
268
+
269
+ expect(event.input).toEqual(partialInput)
270
+ expect(event.input.title).toBe('New Title')
271
+ expect(event.input.year).toBe(2025)
272
+ })
273
+
274
+ it('should update existing in meta', () => {
275
+ const input: IBookInput = {
276
+ title: 'Test Book',
277
+ description: 'A book for testing',
278
+ author: 'Author Name',
279
+ year: 2024,
280
+ }
281
+ const existing: IBookResult = {
282
+ id: '1',
283
+ title: 'Old Title',
284
+ description: 'Old Description',
285
+ author: 'Old Author',
286
+ year: 2000,
287
+ }
288
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'update' }
289
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input, { existing })
290
+
291
+ const newExisting: IBookResult = {
292
+ id: '1',
293
+ title: 'Different Title',
294
+ description: 'Different Description',
295
+ author: 'Different Author',
296
+ year: 2010,
297
+ }
298
+ event.setMeta({ existing: newExisting })
299
+
300
+ expect(event.meta.existing).toEqual(newExisting)
301
+ })
302
+
303
+ it('should handle delete action with existing record', () => {
304
+ const input = { id: '1' }
305
+ const existing: IBookResult = {
306
+ id: '1',
307
+ title: 'Book to Delete',
308
+ description: 'This will be deleted',
309
+ author: 'Some Author',
310
+ year: 2020,
311
+ }
312
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'delete' }
313
+ const event = new MutationEvent<IBookResult, { id: string }>(descriptor, input, { existing })
314
+
315
+ expect(event.descriptor.action).toBe('delete')
316
+ expect(event.meta.existing).toEqual(existing)
317
+ expect(event.input).toEqual(input)
318
+ })
319
+
320
+ it('should maintain type throughout lifecycle', () => {
321
+ const input: IBookInput = {
322
+ title: 'Test Book',
323
+ description: 'A book for testing',
324
+ author: 'Author Name',
325
+ year: 2024,
326
+ }
327
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
328
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
329
+
330
+ expect(event.type).toBe('books::book.create')
331
+
332
+ event.setInput({ ...input, title: 'Updated' })
333
+ expect(event.type).toBe('books::book.create')
334
+
335
+ const result: IBookResult = {
336
+ id: '1',
337
+ title: 'Updated',
338
+ description: 'A book for testing',
339
+ author: 'Author Name',
340
+ year: 2024,
341
+ }
35
342
  event.setResult(result)
343
+ expect(event.type).toBe('books::book.create')
344
+ })
345
+
346
+ it('should serialize existing to JSON for update action', () => {
347
+ const input: IBookInput = {
348
+ title: 'Updated Title',
349
+ description: 'Updated description',
350
+ author: 'Author Name',
351
+ year: 2024,
352
+ }
353
+ const existing: IBookResult = {
354
+ id: '1',
355
+ title: 'Old Title',
356
+ description: 'Old Description',
357
+ author: 'Old Author',
358
+ year: 2020,
359
+ }
360
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'update' }
361
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input, { existing })
362
+
363
+ const json = event.toJSON()
364
+
365
+ expect(json.meta.existing).toEqual(existing)
366
+ expect(json.input).toEqual(input)
367
+ })
368
+
369
+ it('should handle create action without existing', () => {
370
+ const input: IBookInput = {
371
+ title: 'New Book',
372
+ description: 'Brand new',
373
+ author: 'New Author',
374
+ year: 2024,
375
+ }
376
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'create' }
377
+ const event = new MutationEvent<IBookResult, IBookInput>(descriptor, input)
378
+
379
+ expect(event.meta.existing).toBeUndefined()
380
+ expect(event.descriptor.action).toBe('create')
381
+
382
+ const result: IBookResult = {
383
+ id: '1',
384
+ ...input,
385
+ }
386
+ event.setResult(result)
387
+
36
388
  expect(event.data).toEqual(result)
37
389
  })
38
390
  })
@@ -1,7 +1,15 @@
1
1
  import type { IActionDescriptorInput } from '@declaro/core'
2
- import { RequestEvent } from './request-event'
2
+ import { RequestEvent, type IRequestEventMeta } from './request-event'
3
3
 
4
- export class MutationEvent<TResult, TInput, TMeta = any> extends RequestEvent<TResult, TInput, TMeta> {
4
+ export interface IMutationEventMeta<TResult> extends IRequestEventMeta {
5
+ existing?: TResult
6
+ }
7
+
8
+ export class MutationEvent<
9
+ TResult,
10
+ TInput,
11
+ TMeta extends IMutationEventMeta<TResult> = IMutationEventMeta<TResult>,
12
+ > extends RequestEvent<TResult, TInput, TMeta> {
5
13
  constructor(descriptor: IActionDescriptorInput, input: TInput, meta: TMeta = {} as TMeta) {
6
14
  super(descriptor, input, meta)
7
15
  }