@api-client/core 0.18.17 → 0.18.18

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 (46) hide show
  1. package/build/src/{modeling → decorators}/observed.d.ts +3 -3
  2. package/build/src/decorators/observed.d.ts.map +1 -0
  3. package/build/src/{modeling → decorators}/observed.js +4 -4
  4. package/build/src/decorators/observed.js.map +1 -0
  5. package/build/src/modeling/ApiModel.js +1 -1
  6. package/build/src/modeling/ApiModel.js.map +1 -1
  7. package/build/src/modeling/DomainAssociation.d.ts +7 -0
  8. package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
  9. package/build/src/modeling/DomainAssociation.js +44 -1
  10. package/build/src/modeling/DomainAssociation.js.map +1 -1
  11. package/build/src/modeling/DomainEntity.d.ts +6 -0
  12. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  13. package/build/src/modeling/DomainEntity.js +21 -1
  14. package/build/src/modeling/DomainEntity.js.map +1 -1
  15. package/build/src/modeling/DomainModel.js +1 -1
  16. package/build/src/modeling/DomainModel.js.map +1 -1
  17. package/build/src/modeling/DomainNamespace.js +1 -1
  18. package/build/src/modeling/DomainNamespace.js.map +1 -1
  19. package/build/src/modeling/DomainProperty.d.ts +5 -0
  20. package/build/src/modeling/DomainProperty.d.ts.map +1 -1
  21. package/build/src/modeling/DomainProperty.js +38 -1
  22. package/build/src/modeling/DomainProperty.js.map +1 -1
  23. package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
  24. package/build/src/modeling/DomainSerialization.js +2 -2
  25. package/build/src/modeling/DomainSerialization.js.map +1 -1
  26. package/build/src/models/Thing.js +1 -1
  27. package/build/src/models/Thing.js.map +1 -1
  28. package/build/tsconfig.tsbuildinfo +1 -1
  29. package/data/models/example-generator-api.json +6 -6
  30. package/package.json +2 -1
  31. package/src/{modeling → decorators}/observed.ts +5 -5
  32. package/src/modeling/ApiModel.ts +1 -1
  33. package/src/modeling/DomainAssociation.ts +51 -1
  34. package/src/modeling/DomainEntity.ts +24 -1
  35. package/src/modeling/DomainModel.ts +1 -1
  36. package/src/modeling/DomainNamespace.ts +1 -1
  37. package/src/modeling/DomainProperty.ts +43 -1
  38. package/src/modeling/DomainSerialization.ts +2 -4
  39. package/src/models/Thing.ts +1 -1
  40. package/tests/unit/decorators/observed.spec.ts +527 -0
  41. package/tests/unit/modeling/data_domain_serialization.spec.ts +6 -2
  42. package/tests/unit/modeling/domain_asociation.spec.ts +376 -0
  43. package/tests/unit/modeling/domain_entity.spec.ts +147 -0
  44. package/tests/unit/modeling/domain_property.spec.ts +273 -0
  45. package/build/src/modeling/observed.d.ts.map +0 -1
  46. package/build/src/modeling/observed.js.map +0 -1
@@ -0,0 +1,527 @@
1
+ import { test } from '@japa/runner'
2
+ import { observed, retargetChange, toRaw } from '../../../src/decorators/observed.js'
3
+
4
+ test.group('observed decorator', () => {
5
+ test('should decorate accessor property and track changes', ({ assert }) => {
6
+ let domainNotifyCallCount = 0
7
+ const mockDomain = {
8
+ notifyChange() {
9
+ domainNotifyCallCount++
10
+ },
11
+ }
12
+
13
+ class TestClass {
14
+ domain = mockDomain
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ @observed() accessor testProperty: any
18
+ }
19
+
20
+ const instance = new TestClass()
21
+
22
+ // Setting property should trigger domain notification
23
+ instance.testProperty = 'test value'
24
+ assert.equal(domainNotifyCallCount, 1)
25
+ assert.equal(instance.testProperty, 'test value')
26
+
27
+ // Setting same value should not trigger notification
28
+ instance.testProperty = 'test value'
29
+ assert.equal(domainNotifyCallCount, 1)
30
+
31
+ // Setting different value should trigger notification
32
+ instance.testProperty = 'new value'
33
+ assert.equal(domainNotifyCallCount, 2)
34
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
35
+
36
+ test('should use notifyChange method on instance if domain is not available', ({ assert }) => {
37
+ let instanceNotifyCallCount = 0
38
+
39
+ class TestClass {
40
+ notifyChange() {
41
+ instanceNotifyCallCount++
42
+ }
43
+
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ @observed() accessor testProperty: any
46
+ }
47
+
48
+ const instance = new TestClass()
49
+
50
+ instance.testProperty = 'test value'
51
+ assert.equal(instanceNotifyCallCount, 1)
52
+ assert.equal(instance.testProperty, 'test value')
53
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
54
+
55
+ test('should work with setter decorator', ({ assert }) => {
56
+ let domainNotifyCallCount = 0
57
+ const mockDomain = {
58
+ notifyChange() {
59
+ domainNotifyCallCount++
60
+ },
61
+ }
62
+
63
+ class TestClass {
64
+ #value: string | undefined
65
+ domain = mockDomain
66
+
67
+ @observed()
68
+ set testProperty(value: string | undefined) {
69
+ this.#value = value
70
+ }
71
+
72
+ get testProperty() {
73
+ return this.#value
74
+ }
75
+ }
76
+
77
+ const instance = new TestClass()
78
+
79
+ instance.testProperty = 'test value'
80
+ assert.equal(domainNotifyCallCount, 1)
81
+ assert.equal(instance.testProperty, 'test value')
82
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
83
+
84
+ test('should support deep observation of objects', ({ assert }) => {
85
+ let domainNotifyCallCount = 0
86
+ const mockDomain = {
87
+ notifyChange() {
88
+ domainNotifyCallCount++
89
+ },
90
+ }
91
+
92
+ class TestClass {
93
+ domain = mockDomain
94
+
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ @observed({ deep: true }) accessor nestedData: any
97
+ }
98
+
99
+ const instance = new TestClass()
100
+ const testObj = { nested: { value: 'initial' } }
101
+
102
+ // Setting object should trigger notification
103
+ instance.nestedData = testObj
104
+ assert.equal(domainNotifyCallCount, 1)
105
+
106
+ // Modifying deep property should trigger notification
107
+ instance.nestedData.nested.value = 'changed'
108
+ assert.equal(domainNotifyCallCount, 2)
109
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
110
+
111
+ test('should handle primitive values with deep observation', ({ assert }) => {
112
+ let domainNotifyCallCount = 0
113
+ const mockDomain = {
114
+ notifyChange() {
115
+ domainNotifyCallCount++
116
+ },
117
+ }
118
+
119
+ class TestClass {
120
+ domain = mockDomain
121
+
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ @observed({ deep: true }) accessor primitiveValue: any
124
+ }
125
+
126
+ const instance = new TestClass()
127
+
128
+ // Setting primitive values should work normally
129
+ instance.primitiveValue = 42
130
+ assert.equal(domainNotifyCallCount, 1)
131
+ assert.equal(instance.primitiveValue, 42)
132
+
133
+ instance.primitiveValue = 'string'
134
+ assert.equal(domainNotifyCallCount, 2)
135
+ assert.equal(instance.primitiveValue, 'string')
136
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
137
+
138
+ test('should handle null and undefined values correctly', ({ assert }) => {
139
+ let domainNotifyCallCount = 0
140
+ const mockDomain = {
141
+ notifyChange() {
142
+ domainNotifyCallCount++
143
+ },
144
+ }
145
+
146
+ class TestClass {
147
+ domain = mockDomain
148
+
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ @observed() accessor nullableValue: any
151
+ }
152
+
153
+ const instance = new TestClass()
154
+
155
+ instance.nullableValue = null
156
+ assert.equal(domainNotifyCallCount, 1)
157
+ assert.equal(instance.nullableValue, null)
158
+
159
+ instance.nullableValue = undefined
160
+ assert.equal(domainNotifyCallCount, 2)
161
+ assert.equal(instance.nullableValue, undefined)
162
+
163
+ instance.nullableValue = 'value'
164
+ assert.equal(domainNotifyCallCount, 3)
165
+ assert.equal(instance.nullableValue, 'value')
166
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
167
+
168
+ test('should throw error for unsupported decorator locations', ({ assert }) => {
169
+ assert.throws(() => {
170
+ const decorator = observed()
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ decorator({} as any, { kind: 'field' } as any)
173
+ }, 'Unsupported decorator location: field')
174
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
175
+
176
+ test('should maintain proxy references correctly with deep observation', ({ assert }) => {
177
+ let domainNotifyCallCount = 0
178
+ const mockDomain = {
179
+ notifyChange() {
180
+ domainNotifyCallCount++
181
+ },
182
+ }
183
+
184
+ class TestClass {
185
+ domain = mockDomain
186
+
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ @observed({ deep: true }) accessor complexData: any
189
+ }
190
+
191
+ const instance = new TestClass()
192
+ const originalObj = { items: [{ name: 'item1' }] }
193
+
194
+ instance.complexData = originalObj
195
+ assert.equal(domainNotifyCallCount, 1)
196
+
197
+ // Modifying array items should trigger notifications
198
+ instance.complexData.items.push({ name: 'item2' })
199
+ assert.equal(domainNotifyCallCount, 2)
200
+
201
+ // Note: the deep proxy system returns original objects for already-accessed objects
202
+ // so this modification may not trigger additional notifications
203
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
204
+
205
+ test('should handle class without domain or notifyChange', ({ assert }) => {
206
+ class TestClass {
207
+ // @ts-expect-error It is for testing purposes
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ @observed() accessor testProperty: any
210
+ }
211
+
212
+ const instance = new TestClass()
213
+
214
+ // Should not throw error when setting value
215
+ assert.doesNotThrow(() => {
216
+ instance.testProperty = 'test value'
217
+ })
218
+ assert.equal(instance.testProperty, 'test value')
219
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
220
+
221
+ test('should handle multiple observed properties on same instance', ({ assert }) => {
222
+ let domainNotifyCallCount = 0
223
+ const mockDomain = {
224
+ notifyChange() {
225
+ domainNotifyCallCount++
226
+ },
227
+ }
228
+
229
+ class TestClass {
230
+ domain = mockDomain
231
+
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ @observed() accessor prop1: any
234
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
+ @observed() accessor prop2: any
236
+ }
237
+
238
+ const instance = new TestClass()
239
+
240
+ instance.prop1 = 'value1'
241
+ assert.equal(domainNotifyCallCount, 1)
242
+
243
+ instance.prop2 = 'value2'
244
+ assert.equal(domainNotifyCallCount, 2)
245
+
246
+ assert.equal(instance.prop1, 'value1')
247
+ assert.equal(instance.prop2, 'value2')
248
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
249
+
250
+ test('should handle circular references in deep observation', ({ assert }) => {
251
+ let notifyChangeCallCount = 0
252
+ const mockDomain = {
253
+ notifyChange: () => {
254
+ notifyChangeCallCount++
255
+ },
256
+ }
257
+
258
+ class TestClass {
259
+ domain = mockDomain
260
+
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ @observed({ deep: true }) accessor data: any
263
+ }
264
+
265
+ const instance = new TestClass()
266
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
267
+ const obj: any = { name: 'test' }
268
+ obj.self = obj // Create circular reference
269
+
270
+ // Should not crash with circular reference
271
+ assert.doesNotThrow(() => {
272
+ instance.data = obj
273
+ })
274
+
275
+ assert.equal(notifyChangeCallCount, 1)
276
+ assert.equal(instance.data.name, 'test')
277
+ // Circular reference is preserved but proxy system prevents infinite loops
278
+ assert.equal(instance.data.self.name, 'test')
279
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
280
+ })
281
+
282
+ test.group('retargetChange decorator', () => {
283
+ class MockEventTarget {
284
+ listeners: ((event: Event) => void)[] = []
285
+
286
+ addEventListener(type: string, listener: (event: Event) => void) {
287
+ if (type === 'change') {
288
+ this.listeners.push(listener)
289
+ }
290
+ }
291
+
292
+ removeEventListener(type: string, listener: (event: Event) => void) {
293
+ if (type === 'change') {
294
+ const index = this.listeners.indexOf(listener)
295
+ if (index > -1) {
296
+ this.listeners.splice(index, 1)
297
+ }
298
+ }
299
+ }
300
+
301
+ triggerChange() {
302
+ const event = new Event('change')
303
+ this.listeners.forEach((listener) => listener(event))
304
+ }
305
+ }
306
+
307
+ test('should retarget change events from EventTarget properties', ({ assert }) => {
308
+ let domainNotifyCallCount = 0
309
+ const mockDomain = {
310
+ notifyChange() {
311
+ domainNotifyCallCount++
312
+ },
313
+ }
314
+
315
+ class TestClass {
316
+ domain = mockDomain
317
+
318
+ @retargetChange()
319
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
+ accessor target: any
321
+ }
322
+
323
+ const instance = new TestClass()
324
+ const mockTarget = new MockEventTarget()
325
+
326
+ // Set EventTarget (accessor version notifies)
327
+ instance.target = mockTarget
328
+ assert.equal(domainNotifyCallCount, 1)
329
+
330
+ // Trigger change event should notify domain
331
+ mockTarget.triggerChange()
332
+ assert.equal(domainNotifyCallCount, 2)
333
+ }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])
334
+
335
+ test('should handle setting same EventTarget value', ({ assert }) => {
336
+ let domainNotifyCallCount = 0
337
+ const mockDomain = {
338
+ notifyChange() {
339
+ domainNotifyCallCount++
340
+ },
341
+ }
342
+
343
+ class TestClass {
344
+ domain = mockDomain
345
+
346
+ @retargetChange()
347
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
348
+ accessor target: any
349
+ }
350
+
351
+ const instance = new TestClass()
352
+ const mockTarget = new MockEventTarget()
353
+
354
+ // Set EventTarget
355
+ instance.target = mockTarget
356
+ assert.equal(domainNotifyCallCount, 1)
357
+
358
+ // Set same EventTarget should not notify again
359
+ instance.target = mockTarget
360
+ assert.equal(domainNotifyCallCount, 1)
361
+ }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])
362
+
363
+ test('should work with setter decorator', ({ assert }) => {
364
+ let domainNotifyCallCount = 0
365
+ const mockDomain = {
366
+ notifyChange() {
367
+ domainNotifyCallCount++
368
+ },
369
+ }
370
+
371
+ class TestClass {
372
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
+ #target: any
374
+ domain = mockDomain
375
+
376
+ @retargetChange()
377
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
378
+ set target(value: any) {
379
+ this.#target = value
380
+ }
381
+
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ get target(): any {
384
+ return this.#target
385
+ }
386
+ }
387
+
388
+ const instance = new TestClass()
389
+ const mockTarget = new MockEventTarget()
390
+
391
+ // Set EventTarget (setter doesn't notify by itself)
392
+ instance.target = mockTarget
393
+ assert.equal(domainNotifyCallCount, 0)
394
+
395
+ // Trigger change event should notify domain
396
+ mockTarget.triggerChange()
397
+ assert.equal(domainNotifyCallCount, 1)
398
+ }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])
399
+
400
+ test('should throw error for unsupported decorator locations', ({ assert }) => {
401
+ assert.throws(() => {
402
+ const decorator = retargetChange()
403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
404
+ decorator({} as any, { kind: 'field' } as any)
405
+ }, 'Unsupported decorator location: field')
406
+ }).tags(['@decorators', '@retargetChange', '@core', '@unit', '@fast'])
407
+ })
408
+
409
+ test.group('toRaw function', () => {
410
+ test('should return raw object from proxy', ({ assert }) => {
411
+ const mockDomain = {
412
+ notifyChange() {
413
+ // ...
414
+ },
415
+ }
416
+
417
+ class TestClass {
418
+ domain = mockDomain
419
+
420
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
421
+ @observed({ deep: true }) accessor data: any
422
+ }
423
+
424
+ const instance = new TestClass()
425
+ const originalObj = { name: 'test' }
426
+
427
+ instance.data = originalObj
428
+
429
+ // toRaw should return the original object
430
+ const raw = toRaw(instance, instance.data)
431
+ assert.strictEqual(raw, originalObj)
432
+ }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])
433
+
434
+ test('should return undefined if no proxy found', ({ assert }) => {
435
+ const instance = {}
436
+ const nonProxiedObj = { name: 'test' }
437
+
438
+ const raw = toRaw(instance, nonProxiedObj)
439
+ assert.equal(raw, undefined)
440
+ }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])
441
+
442
+ test('should return undefined if proxy symbol not found', ({ assert }) => {
443
+ const instance = {}
444
+ const obj = { name: 'test' }
445
+
446
+ const raw = toRaw(instance, obj)
447
+ assert.equal(raw, undefined)
448
+ }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])
449
+ })
450
+
451
+ test.group('edge cases and error conditions', () => {
452
+ test('should handle class without domain or notifyChange', ({ assert }) => {
453
+ class TestClass {
454
+ // @ts-expect-error It is for testing purposes
455
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
456
+ @observed() accessor testProperty: any
457
+ }
458
+
459
+ const instance = new TestClass()
460
+
461
+ // Should not throw error when setting value
462
+ assert.doesNotThrow(() => {
463
+ instance.testProperty = 'test value'
464
+ })
465
+ assert.equal(instance.testProperty, 'test value')
466
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
467
+
468
+ test('should handle multiple observed properties on same instance', ({ assert }) => {
469
+ let domainNotifyCallCount = 0
470
+ const mockDomain = {
471
+ notifyChange() {
472
+ domainNotifyCallCount++
473
+ },
474
+ }
475
+
476
+ class TestClass {
477
+ domain = mockDomain
478
+
479
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
480
+ @observed() accessor prop1: any
481
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
482
+ @observed() accessor prop2: any
483
+ }
484
+
485
+ const instance = new TestClass()
486
+
487
+ instance.prop1 = 'value1'
488
+ assert.equal(domainNotifyCallCount, 1)
489
+
490
+ instance.prop2 = 'value2'
491
+ assert.equal(domainNotifyCallCount, 2)
492
+
493
+ assert.equal(instance.prop1, 'value1')
494
+ assert.equal(instance.prop2, 'value2')
495
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
496
+
497
+ test('should handle circular references in deep observation', ({ assert }) => {
498
+ let notifyChangeCallCount = 0
499
+ const mockDomain = {
500
+ notifyChange: () => {
501
+ notifyChangeCallCount++
502
+ },
503
+ }
504
+
505
+ class TestClass {
506
+ domain = mockDomain
507
+
508
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
509
+ @observed({ deep: true }) accessor data: any
510
+ }
511
+
512
+ const instance = new TestClass()
513
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
514
+ const obj: any = { name: 'test' }
515
+ obj.self = obj // Create circular reference
516
+
517
+ // Should not crash with circular reference
518
+ assert.doesNotThrow(() => {
519
+ instance.data = obj
520
+ })
521
+
522
+ assert.equal(notifyChangeCallCount, 1)
523
+ assert.equal(instance.data.name, 'test')
524
+ // Circular reference is preserved but proxy system prevents infinite loops
525
+ assert.equal(instance.data.self.name, 'test')
526
+ }).tags(['@decorators', '@observed', '@core', '@unit', '@fast'])
527
+ })
@@ -456,7 +456,7 @@ test.group('Validation Tests', () => {
456
456
  assert.throws(() => domain.toJSON())
457
457
  }).tags(['@modeling', '@serialization', '@validation'])
458
458
 
459
- test('should throw validation error when association has no target entities', ({ assert }) => {
459
+ test('should not throw validation error when association has no target entities', ({ assert }) => {
460
460
  const domain = new DataDomain()
461
461
  const m1 = domain.addModel()
462
462
  const e1 = m1.addEntity()
@@ -466,7 +466,11 @@ test.group('Validation Tests', () => {
466
466
  // Remove the association target edge
467
467
  domain.graph.removeEdge(a1.key, e2.key)
468
468
 
469
- assert.throws(() => domain.toJSON())
469
+ // Note, we specifically removed this from the validation as it would throw errors
470
+ // when associations were created without targets. This is not a structural issue and because
471
+ // of that, it shouldn't be considered a validation error in this context.
472
+ // It is reported through the validator, though.
473
+ assert.doesNotThrow(() => domain.toJSON())
470
474
  }).tags(['@modeling', '@serialization', '@validation'])
471
475
 
472
476
  test('should throw validation error when association references non-existent target', ({ assert }) => {