@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,28 +1,228 @@
1
1
  import { describe, it, expect } from 'bun:test'
2
2
  import { QueryEvent } from './query-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 ILoadParams {
14
+ id: string
15
+ }
16
+
17
+ interface ISearchParams {
18
+ text?: string
19
+ author?: string
20
+ year?: number
21
+ }
22
+
23
+ interface ICustomMeta {
24
+ customField?: string
25
+ anotherField?: number
26
+ }
3
27
 
4
28
  describe('QueryEvent', () => {
5
- describe('load action', () => {
6
- it('should lookup a book with the given lookup', () => {
7
- const params = { id: 42 }
8
- const descriptor = { namespace: 'books', action: 'load' }
9
- const query = new QueryEvent(descriptor, params, {})
10
-
11
- expect(query.descriptor.namespace).toBe('books')
12
- expect(query.descriptor.action).toBe('load')
13
- expect(query.meta.input).toEqual(params)
14
- })
29
+ it('should create a query event with params and descriptor', () => {
30
+ const params: ILoadParams = { id: '1' }
31
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
32
+ const event = new QueryEvent<IBookResult, ILoadParams>(descriptor, params)
33
+
34
+ expect(event.input).toEqual(params)
35
+ expect(event.descriptor.toString()).toEqual(new ActionDescriptor(descriptor).toString())
36
+ expect(event.meta).toEqual({})
37
+ expect(event.data).toBeUndefined()
38
+ })
39
+
40
+ it('should set result correctly', () => {
41
+ const params: ILoadParams = { id: '1' }
42
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
43
+ const event = new QueryEvent<IBookResult, ILoadParams>(descriptor, params)
44
+
45
+ expect(event.data).toBeUndefined()
46
+
47
+ const result: IBookResult = {
48
+ id: '1',
49
+ title: 'Test Book',
50
+ description: 'A book for testing',
51
+ author: 'Author Name',
52
+ year: 2024,
53
+ }
54
+ event.setResult(result)
55
+
56
+ expect(event.data).toEqual(result)
57
+ })
58
+
59
+ it('should update meta correctly', () => {
60
+ const params: ISearchParams = { text: '1984' }
61
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
62
+ const event = new QueryEvent<IBookResult[], ISearchParams, ICustomMeta>(descriptor, params)
63
+
64
+ event.setMeta({ customField: 'custom value' })
65
+
66
+ expect(event.meta.customField).toBe('custom value')
67
+ })
68
+
69
+ it('should serialize to JSON with eventId and timestamp', () => {
70
+ const params: ILoadParams = { id: '1' }
71
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
72
+ const event = new QueryEvent<IBookResult, ILoadParams>(descriptor, params)
73
+
74
+ const result: IBookResult = {
75
+ id: '1',
76
+ title: 'Test Book',
77
+ description: 'A book for testing',
78
+ author: 'Author Name',
79
+ year: 2024,
80
+ }
81
+ event.setResult(result)
82
+
83
+ const json = event.toJSON()
84
+
85
+ expect(json.eventId).toBeDefined()
86
+ expect(json.timestamp).toBeDefined()
87
+ expect(json.type).toBe('books::book.load')
88
+ expect(json.input).toEqual(params)
89
+ expect(json.data).toEqual(result)
90
+ })
91
+
92
+ it('should handle chaining of setter methods', () => {
93
+ const params: ILoadParams = { id: '1' }
94
+ const result: IBookResult = {
95
+ id: '1',
96
+ title: 'Test Book',
97
+ description: 'A book for testing',
98
+ author: 'Author Name',
99
+ year: 2024,
100
+ }
101
+
102
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
103
+ const event = new QueryEvent<IBookResult, ILoadParams, ICustomMeta>(descriptor, params)
104
+ .setMeta({ customField: 'value' })
105
+ .setResult(result)
106
+
107
+ expect(event.input).toEqual(params)
108
+ expect(event.meta.customField).toBe('value')
109
+ expect(event.data).toEqual(result)
15
110
  })
16
111
 
17
- describe('search action', () => {
18
- it('should search for books with the given filters', () => {
19
- const params = { text: '1984' }
20
- const descriptor = { namespace: 'books', action: 'search' }
21
- const query = new QueryEvent(descriptor, params, {})
112
+ it('should update input after initialization', () => {
113
+ const params: ISearchParams = { text: '1984' }
114
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
115
+ const event = new QueryEvent<IBookResult[], ISearchParams>(descriptor, params)
22
116
 
23
- expect(query.descriptor.namespace).toBe('books')
24
- expect(query.descriptor.action).toBe('search')
25
- expect(query.meta.input).toEqual(params)
117
+ const newParams: ISearchParams = {
118
+ text: 'Brave New World',
119
+ author: 'Aldous Huxley',
120
+ }
121
+ event.setInput(newParams)
122
+
123
+ expect(event.input).toEqual(newParams)
124
+ })
125
+
126
+ it('should maintain type throughout lifecycle', () => {
127
+ const params: ILoadParams = { id: '1' }
128
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
129
+ const event = new QueryEvent<IBookResult, ILoadParams>(descriptor, params)
130
+
131
+ expect(event.type).toBe('books::book.load')
132
+
133
+ event.setInput({ id: '2' })
134
+ expect(event.type).toBe('books::book.load')
135
+
136
+ const result: IBookResult = {
137
+ id: '2',
138
+ title: 'Another Book',
139
+ description: 'Another description',
140
+ author: 'Another Author',
141
+ year: 2025,
142
+ }
143
+ event.setResult(result)
144
+ expect(event.type).toBe('books::book.load')
145
+ })
146
+
147
+ it('should handle search action with multiple filters', () => {
148
+ const params: ISearchParams = {
149
+ text: 'science fiction',
150
+ author: 'Isaac Asimov',
151
+ year: 1950,
152
+ }
153
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
154
+ const event = new QueryEvent<IBookResult[], ISearchParams>(descriptor, params)
155
+
156
+ expect(event.descriptor.action).toBe('search')
157
+ expect(event.input).toEqual(params)
158
+ })
159
+
160
+ it('should handle search results as array', () => {
161
+ const params: ISearchParams = { text: 'Foundation' }
162
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
163
+ const event = new QueryEvent<IBookResult[], ISearchParams>(descriptor, params)
164
+
165
+ const results: IBookResult[] = [
166
+ {
167
+ id: '1',
168
+ title: 'Foundation',
169
+ description: 'First book',
170
+ author: 'Isaac Asimov',
171
+ year: 1951,
172
+ },
173
+ {
174
+ id: '2',
175
+ title: 'Foundation and Empire',
176
+ description: 'Second book',
177
+ author: 'Isaac Asimov',
178
+ year: 1952,
179
+ },
180
+ ]
181
+ event.setResult(results)
182
+
183
+ expect(event.data).toEqual(results)
184
+ expect(event.data?.length).toBe(2)
185
+ })
186
+
187
+ it('should handle load action with single result', () => {
188
+ const params: ILoadParams = { id: '42' }
189
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'load' }
190
+ const event = new QueryEvent<IBookResult, ILoadParams>(descriptor, params)
191
+
192
+ const result: IBookResult = {
193
+ id: '42',
194
+ title: "The Hitchhiker's Guide to the Galaxy",
195
+ description: "Don't Panic",
196
+ author: 'Douglas Adams',
197
+ year: 1979,
198
+ }
199
+ event.setResult(result)
200
+
201
+ expect(event.data).toEqual(result)
202
+ expect(event.descriptor.action).toBe('load')
203
+ })
204
+
205
+ it('should serialize meta to JSON', () => {
206
+ const params: ISearchParams = { text: 'test' }
207
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
208
+ const event = new QueryEvent<IBookResult[], ISearchParams, ICustomMeta>(descriptor, params, {
209
+ customField: 'custom value',
210
+ anotherField: 123,
26
211
  })
212
+
213
+ const json = event.toJSON()
214
+
215
+ expect(json.meta.customField).toBe('custom value')
216
+ expect(json.meta.anotherField).toBe(123)
217
+ expect(json.input).toEqual(params)
218
+ })
219
+
220
+ it('should handle empty search params', () => {
221
+ const params: ISearchParams = {}
222
+ const descriptor: IActionDescriptorInput = { namespace: 'books', resource: 'book', action: 'search' }
223
+ const event = new QueryEvent<IBookResult[], ISearchParams>(descriptor, params)
224
+
225
+ expect(event.input).toEqual({})
226
+ expect(event.descriptor.action).toBe('search')
27
227
  })
28
228
  })
@@ -1,7 +1,13 @@
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 QueryEvent<TResult, TParams, TMeta = any> extends RequestEvent<TResult, TParams, TMeta> {
4
+ export interface IQueryEventMeta<TResult> extends IRequestEventMeta {}
5
+
6
+ export class QueryEvent<
7
+ TResult,
8
+ TParams,
9
+ TMeta extends IQueryEventMeta<TResult> = IQueryEventMeta<TResult>,
10
+ > extends RequestEvent<TResult, TParams, TMeta> {
5
11
  constructor(descriptor: IActionDescriptorInput, params: TParams, meta: TMeta = {} as TMeta) {
6
12
  super(descriptor, params, meta)
7
13
  }
@@ -10,7 +10,7 @@ describe('RequestEvent', () => {
10
10
  })
11
11
 
12
12
  expect(event.meta.foo).toBe('bar')
13
- expect(event.meta.input?.key).toBe('value')
13
+ expect(event.input?.key).toBe('value')
14
14
  })
15
15
 
16
16
  it('should update meta correctly', () => {
@@ -1,22 +1,30 @@
1
1
  import type { IActionDescriptorInput, Simplify } from '@declaro/core'
2
2
  import { DomainEvent } from './domain-event'
3
3
 
4
- export interface IRequestEventInput<TInput> {
4
+ export interface IRequestEventMeta {}
5
+
6
+ export interface IRequestEventJSON<TInput, TMeta = IRequestEventMeta> extends Simplify<DomainEvent<TInput, TMeta>> {
5
7
  input: TInput
6
8
  }
7
9
 
8
- export class RequestEvent<TResult, TInput, TMeta = any> extends DomainEvent<
10
+ export class RequestEvent<TResult, TInput, TMeta extends IRequestEventMeta = IRequestEventMeta> extends DomainEvent<
9
11
  TResult,
10
- Simplify<IRequestEventInput<TInput> & TMeta>
12
+ TMeta
11
13
  > {
14
+ input: TInput
15
+
12
16
  constructor(descriptor: IActionDescriptorInput, input: TInput, meta: TMeta = {} as TMeta) {
13
17
  super({
14
- meta: {
15
- ...meta,
16
- input,
17
- },
18
+ meta,
18
19
  descriptor,
19
20
  })
21
+
22
+ this.input = input
23
+ }
24
+
25
+ setInput(input: TInput): this {
26
+ this.input = input
27
+ return this
20
28
  }
21
29
 
22
30
  setMeta(meta: Partial<TMeta>): this {
@@ -29,4 +37,11 @@ export class RequestEvent<TResult, TInput, TMeta = any> extends DomainEvent<
29
37
 
30
38
  return this
31
39
  }
40
+
41
+ toJSON() {
42
+ return {
43
+ ...super.toJSON(),
44
+ input: this.input,
45
+ }
46
+ }
32
47
  }
@@ -104,4 +104,33 @@ export interface IRepository<TSchema extends AnyModelSchema> {
104
104
  * @returns A promise resolving to the count of matching elements.
105
105
  */
106
106
  count(search: InferFilters<TSchema>, options?: ISearchOptions<TSchema>): Promise<number>
107
+
108
+ /**
109
+ * Permanently deletes all items from trash, optionally filtered by the provided criteria.
110
+ * Items deleted via this method cannot be restored.
111
+ *
112
+ * @param filters - Optional filters to apply when selecting items to delete from trash.
113
+ * @returns A promise resolving to the count of permanently deleted items.
114
+ */
115
+ emptyTrash(filters?: InferFilters<TSchema>): Promise<number>
116
+
117
+ /**
118
+ * Permanently deletes a specific item from trash based on the provided lookup.
119
+ * The item must exist in trash (previously removed). Items deleted via this method cannot be restored.
120
+ *
121
+ * @param lookup - The lookup criteria for the item to permanently delete from trash.
122
+ * @returns A promise resolving to the permanently deleted item summary.
123
+ * @throws Error if the item is not found in trash.
124
+ */
125
+ permanentlyDeleteFromTrash(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>>
126
+
127
+ /**
128
+ * Permanently deletes an item based on the provided lookup, regardless of whether it is active or in trash.
129
+ * Items deleted via this method cannot be restored.
130
+ *
131
+ * @param lookup - The lookup criteria for the item to permanently delete.
132
+ * @returns A promise resolving to the permanently deleted item summary.
133
+ * @throws Error if the item is not found in either active data or trash.
134
+ */
135
+ permanentlyDelete(lookup: InferLookup<TSchema>): Promise<InferSummary<TSchema>>
107
136
  }
@@ -138,13 +138,13 @@ describe('ModelService - Normalization', () => {
138
138
  const beforeCreateCall = beforeCreateSpy.mock.calls[0][0]
139
139
  const afterCreateCall = afterCreateSpy.mock.calls[0][0]
140
140
 
141
- expect(beforeCreateCall.meta.input.title).toBe('Test Book')
142
- expect(beforeCreateCall.meta.input.author).toBe('Author Name')
143
- expect(beforeCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
141
+ expect(beforeCreateCall.input.title).toBe('Test Book')
142
+ expect(beforeCreateCall.input.author).toBe('Author Name')
143
+ expect(beforeCreateCall.input.publishedDate).toEqual(new Date('2023-01-01'))
144
144
 
145
- expect(afterCreateCall.meta.input.title).toBe('Test Book')
146
- expect(afterCreateCall.meta.input.author).toBe('Author Name')
147
- expect(afterCreateCall.meta.input.publishedDate).toEqual(new Date('2023-01-01'))
145
+ expect(afterCreateCall.input.title).toBe('Test Book')
146
+ expect(afterCreateCall.input.author).toBe('Author Name')
147
+ expect(afterCreateCall.input.publishedDate).toEqual(new Date('2023-01-01'))
148
148
  })
149
149
 
150
150
  it('should call normalizeInput method exactly once per input during bulkUpsert with Promise.all', async () => {