@api-client/core 0.11.7 → 0.11.8

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 (53) hide show
  1. package/build/src/browser.d.ts +2 -0
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +2 -0
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/index.d.ts +2 -0
  6. package/build/src/index.d.ts.map +1 -1
  7. package/build/src/index.js +2 -0
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/modeling/DataAssociation.d.ts +8 -0
  10. package/build/src/modeling/DataAssociation.d.ts.map +1 -1
  11. package/build/src/modeling/DataAssociation.js +20 -0
  12. package/build/src/modeling/DataAssociation.js.map +1 -1
  13. package/build/src/modeling/DataEntity.d.ts +17 -1
  14. package/build/src/modeling/DataEntity.d.ts.map +1 -1
  15. package/build/src/modeling/DataEntity.js +48 -6
  16. package/build/src/modeling/DataEntity.js.map +1 -1
  17. package/build/src/modeling/DataModel.d.ts +10 -1
  18. package/build/src/modeling/DataModel.d.ts.map +1 -1
  19. package/build/src/modeling/DataModel.js +22 -2
  20. package/build/src/modeling/DataModel.js.map +1 -1
  21. package/build/src/modeling/DataNamespace.d.ts +60 -55
  22. package/build/src/modeling/DataNamespace.d.ts.map +1 -1
  23. package/build/src/modeling/DataNamespace.js +133 -116
  24. package/build/src/modeling/DataNamespace.js.map +1 -1
  25. package/build/src/modeling/DataProperty.d.ts +16 -3
  26. package/build/src/modeling/DataProperty.d.ts.map +1 -1
  27. package/build/src/modeling/DataProperty.js +28 -2
  28. package/build/src/modeling/DataProperty.js.map +1 -1
  29. package/build/src/modeling/ImpactAnalysis.d.ts +290 -0
  30. package/build/src/modeling/ImpactAnalysis.d.ts.map +1 -0
  31. package/build/src/modeling/ImpactAnalysis.js +437 -0
  32. package/build/src/modeling/ImpactAnalysis.js.map +1 -0
  33. package/build/src/modeling/types.d.ts +14 -0
  34. package/build/src/modeling/types.d.ts.map +1 -0
  35. package/build/src/modeling/types.js +2 -0
  36. package/build/src/modeling/types.js.map +1 -0
  37. package/package.json +6 -6
  38. package/src/modeling/DataAssociation.ts +21 -0
  39. package/src/modeling/DataEntity.ts +59 -10
  40. package/src/modeling/DataModel.ts +24 -2
  41. package/src/modeling/DataNamespace.ts +150 -137
  42. package/src/modeling/DataProperty.ts +32 -3
  43. package/src/modeling/ImpactAnalysis.ts +519 -0
  44. package/src/modeling/types.ts +13 -0
  45. package/tests/servers/ExpressServer.ts +1 -0
  46. package/tests/servers/express-routes/BaseApi.ts +1 -1
  47. package/tests/servers/express-routes/TestsApi.ts +1 -1
  48. package/tests/unit/modeling/data_association.spec.ts +73 -0
  49. package/tests/unit/modeling/data_entity.spec.ts +111 -1
  50. package/tests/unit/modeling/data_model.spec.ts +54 -0
  51. package/tests/unit/modeling/data_namespace.spec.ts +46 -1
  52. package/tests/unit/modeling/data_property.spec.ts +73 -0
  53. package/tests/unit/modeling/impact_analysis.spec.ts +373 -0
@@ -0,0 +1,519 @@
1
+ import {
2
+ DataNamespaceKind,
3
+ DataEntityKind,
4
+ DataModelKind,
5
+ DataPropertyKind,
6
+ DataAssociationKind,
7
+ } from '../models/kinds.js'
8
+ import type { DataNamespace } from './DataNamespace.js'
9
+
10
+ export type ImpactKinds =
11
+ | typeof DataNamespaceKind
12
+ | typeof DataEntityKind
13
+ | typeof DataModelKind
14
+ | typeof DataPropertyKind
15
+ | typeof DataAssociationKind
16
+
17
+ /**
18
+ * The impact analysis report
19
+ */
20
+ interface ImpactReport {
21
+ /**
22
+ * The key of the impacted data object.
23
+ * This is the key of the object that is being changed.
24
+ */
25
+ key: string
26
+ /**
27
+ * The kind of the impacted data object.
28
+ * This is the kind of the object that is being changed.
29
+ */
30
+ kind: ImpactKinds
31
+ /**
32
+ * The list of impacted data objects.
33
+ */
34
+ impact: ImpactItem[]
35
+ /**
36
+ * Whether it is possible to proceed with the change.
37
+ * If the change is not possible, the reason will be in the impact list.
38
+ */
39
+ canProceed: boolean
40
+ }
41
+
42
+ interface ImpactItem {
43
+ /**
44
+ * The key of the impacted data object.
45
+ */
46
+ key: string
47
+ /**
48
+ * The kind of the impacted data object.
49
+ */
50
+ kind: string
51
+ /**
52
+ * The type of the impact.
53
+ *
54
+ * - `delete` - The data object would be deleted.
55
+ */
56
+ type: 'delete'
57
+ /**
58
+ * The impact description.
59
+ */
60
+ impact: string
61
+ /**
62
+ * Whether the impact is blocking the operation.
63
+ * If true, the operation cannot proceed.
64
+ */
65
+ blocking: boolean
66
+ /**
67
+ * The type of the relationship between two impacted objects.
68
+ */
69
+ relationship?: 'child'
70
+ /**
71
+ * The resolution of the conflict if the change will be forced.
72
+ */
73
+ resolution?: string
74
+ }
75
+
76
+ /**
77
+ * # ImpactAnalysis
78
+ *
79
+ * The `ImpactAnalysis` class is a powerful tool for analyzing the consequences of deleting data domain objects
80
+ * within a `DataNamespace`.
81
+ * It helps developers understand the ripple effects of removing a namespace, data model, entity, property,
82
+ * or association, ensuring data integrity and preventing unintended side effects.
83
+ *
84
+ * ## Core Concepts
85
+ *
86
+ * - **Impact Report:** The central output of the `ImpactAnalysis` class is an `ImpactReport`. This report details the
87
+ * potential consequences of a deletion operation, including:
88
+ * - The object being deleted (`key`, `kind`).
89
+ * - A list of `ImpactItem` objects, each describing a specific consequence.
90
+ * - Whether the deletion can proceed safely (`canProceed`).
91
+ *
92
+ * - **ImpactItem:** Each `ImpactItem` describes a specific consequence of the deletion. Key properties include:
93
+ * - `key`: The key of the impacted object.
94
+ * - `kind`: The kind of the impacted object.
95
+ * - `type`: The type of impact (currently only `delete`).
96
+ * - `impact`: A human-readable description of the impact.
97
+ * - `blocking`: Whether this impact prevents the deletion from proceeding.
98
+ * - `relationship`: The type of relationship between the deleted object and the impacted object (e.g., `child`).
99
+ * - `resolution`: A description of how the impact will be resolved if the deletion is forced.
100
+ *
101
+ * - **Blocking Impacts:** Some impacts are considered "blocking," meaning they prevent the deletion from proceeding
102
+ * without manual intervention. For example, deleting an entity that is a parent to other entities is
103
+ * a blocking impact.
104
+ *
105
+ * - **Non-Blocking Impacts:** Some impacts are informational and do not prevent the deletion. For example, deleting a
106
+ * property is not a blocking impact.
107
+ *
108
+ * ## Usage
109
+ *
110
+ * 1. **Instantiation:** Create an instance of `ImpactAnalysis`, passing the root `DataNamespace` as an argument.
111
+ *
112
+ * ```typescript
113
+ * import { DataNamespace } from './DataNamespace'; // Assuming DataNamespace is in the same directory
114
+ * import { ImpactAnalysis } from './ImpactAnalysis'; // Assuming ImpactAnalysis is in the same directory
115
+ *
116
+ * const rootNamespace = new DataNamespace();
117
+ * // ... add some data to the namespace
118
+ * const analyzer = new ImpactAnalysis(rootNamespace);
119
+ * ```
120
+ *
121
+ * 2. **Performing Analysis:** Use the `deleteAnalysis()` method to generate an `ImpactReport` for a specific deletion.
122
+ * Provide the `key` and `kind` of the object to be deleted.
123
+ *
124
+ * ```typescript
125
+ * import { DataEntityKind } from '../models/kinds.js';
126
+ * // ...
127
+ * const entityKey = 'some-entity-key';
128
+ * const report = analyzer.deleteAnalysis(entityKey, DataEntityKind);
129
+ * ```
130
+ *
131
+ * 3. **Interpreting the Report:** Examine the `ImpactReport` to understand the consequences of the deletion.
132
+ * - Check `report.canProceed` to see if the deletion is safe.
133
+ * - Iterate through `report.impact` to understand each consequence.
134
+ * - Pay special attention to `impact.blocking` to identify impacts that require manual resolution.
135
+ *
136
+ * ```typescript
137
+ * if (report.canProceed) {
138
+ * // Proceed with deletion
139
+ * } else {
140
+ * console.warn('Deletion cannot proceed due to the following impacts:');
141
+ * report.impact.forEach((item) => {
142
+ * console.warn(`- ${item.impact}`);
143
+ * if (item.blocking) {
144
+ * console.warn(` - This impact is blocking.`);
145
+ * if (item.resolution) {
146
+ * console.warn(` - Resolution: ${item.resolution}`);
147
+ * }
148
+ * }
149
+ * });
150
+ * // Handle blocking impacts (e.g., prompt the user, modify the data, etc.)
151
+ * }
152
+ * ```
153
+ *
154
+ * ## Supported Deletion Scenarios
155
+ *
156
+ * The `ImpactAnalysis` class supports analyzing the deletion of the following data domain object types:
157
+ *
158
+ * - **Namespaces (`DataNamespaceKind`):** Deleting a namespace also impacts all its child namespaces,
159
+ * data models, entities, properties, and associations.
160
+ * - **Data Models (`DataModelKind`):** Deleting a data model impacts all its entities, properties, and associations.
161
+ * - **Entities (`DataEntityKind`):** Deleting an entity impacts its properties, associations, and any other
162
+ * entities that have it as a parent or target in an association.
163
+ * - **Properties (`DataPropertyKind`):** Deleting a property impacts the entity it belongs to.
164
+ * - **Associations (`DataAssociationKind`):** Deleting an association impacts the entity it belongs to.
165
+ *
166
+ * ## Example: Deleting an Entity
167
+ *
168
+ * Consider the following scenario:
169
+ *
170
+ * - Namespace: `MyNamespace`
171
+ * - Data Model: `ProductModel`
172
+ * - Entity: `Product`
173
+ * - Property: `name`
174
+ * - Association: `category` (targets `Category` entity)
175
+ * - Entity: `Category`
176
+ * - Property: `name`
177
+ * - Entity: `SpecialProduct` (parent: `Product`)
178
+ *
179
+ * If you attempt to delete the `Product` entity, the `ImpactAnalysis` will generate a report similar to this:
180
+ *
181
+ * ```json
182
+ * {
183
+ * "key": "Product",
184
+ * "kind": "DataEntityKind",
185
+ * "impact": [
186
+ * {
187
+ * "key": "Product",
188
+ * "kind": "DataEntityKind",
189
+ * "type": "delete",
190
+ * "impact": "The entity with key Product will be deleted.",
191
+ * "blocking": false
192
+ * },
193
+ * {
194
+ * "key": "SpecialProduct",
195
+ * "kind": "DataEntityKind",
196
+ * "type": "delete",
197
+ * "impact": "The SpecialProduct entity will become an orphan because it is a child of Product.",
198
+ * "resolution": "The Product will be removed as the parent parent of SpecialProduct.",
199
+ * "blocking": true,
200
+ * "relationship": "child"
201
+ * },
202
+ * {
203
+ * "key": "category",
204
+ * "kind": "DataAssociationKind",
205
+ * "type": "delete",
206
+ * "impact": "The association with key category will be broken because it has a target to Product.",
207
+ * "resolution": "The association with key category will be removed from Product.",
208
+ * "blocking": true
209
+ * },
210
+ * {
211
+ * "key": "name",
212
+ * "kind": "DataPropertyKind",
213
+ * "type": "delete",
214
+ * "impact": "The property with key name will be deleted.",
215
+ * "blocking": false
216
+ * }
217
+ * ],
218
+ * "canProceed": false
219
+ * }
220
+ * ```
221
+ *
222
+ * This report indicates that:
223
+ *
224
+ * - The `Product` entity will be deleted.
225
+ * - The `SpecialProduct` entity will become an orphan (blocking).
226
+ * - The `category` association will be broken (blocking).
227
+ * - The `name` property will be deleted.
228
+ * - The deletion cannot proceed without addressing the blocking impacts.
229
+ *
230
+ * ## Types
231
+ *
232
+ * ### `ImpactKinds`
233
+ *
234
+ * - **Description:** A type alias for the kinds of data domain objects that can be analyzed.
235
+ * - **Values:**
236
+ * - `DataNamespaceKind`
237
+ * - `DataEntityKind`
238
+ * - `DataModelKind`
239
+ * - `DataPropertyKind`
240
+ * - `DataAssociationKind`
241
+ *
242
+ * ### `ImpactReport`
243
+ *
244
+ * - **Description:** The structure of the impact analysis report.
245
+ * - **Properties:**
246
+ * - `key` (`string`): The key of the object being deleted.
247
+ * - `kind` (`ImpactKinds`): The kind of the object being deleted.
248
+ * - `impact` (`ImpactItem[]`): The list of impacts.
249
+ * - `canProceed` (`boolean`): Whether the deletion can proceed.
250
+ *
251
+ * ### `ImpactItem`
252
+ *
253
+ * - **Description:** The structure of an individual impact item.
254
+ * - **Properties:**
255
+ * - `key` (`string`): The key of the impacted object.
256
+ * - `kind` (`string`): The kind of the impacted object.
257
+ * - `type` (`'delete'`): The type of impact.
258
+ * - `impact` (`string`): The impact description.
259
+ * - `blocking` (`boolean`): Whether the impact is blocking.
260
+ * - `relationship` (`'child'`, optional): The relationship type.
261
+ * - `resolution` (`string`, optional): The resolution description.
262
+ *
263
+ * ## Error Handling
264
+ *
265
+ * The `ImpactAnalysis` class does not throw errors. Instead, it uses the `ImpactReport` to communicate
266
+ * the results of the analysis, including any blocking impacts.
267
+ *
268
+ * ## Best Practices
269
+ *
270
+ * - **Always Analyze Before Deleting:** Before deleting any data domain object, always use `ImpactAnalysis`
271
+ * to understand the consequences.
272
+ * - **Handle Blocking Impacts:** Pay close attention to `blocking` impacts and implement appropriate
273
+ * logic to handle them.
274
+ * - **Inform the User:** If a deletion cannot proceed, inform the user about the blocking impacts and provide
275
+ * guidance on how to resolve them.
276
+ * - **Consider Forced Deletion:** In some cases, you may want to allow users to force a deletion even if there are
277
+ * blocking impacts. In such cases, you should clearly communicate the risks to the user.
278
+ *
279
+ * ## Conclusion
280
+ *
281
+ * The `ImpactAnalysis` class is an essential tool for maintaining data integrity when working with complex
282
+ * data domain models. By providing detailed impact reports, it empowers developers to make informed decisions
283
+ * about data deletion and prevent unintended consequences.
284
+ */
285
+ export class ImpactAnalysis {
286
+ private report: ImpactReport
287
+ private root: DataNamespace
288
+
289
+ constructor(root: DataNamespace) {
290
+ this.root = root
291
+ this.report = {
292
+ key: '',
293
+ kind: DataNamespaceKind,
294
+ impact: [],
295
+ canProceed: false,
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Generates a report of how the data domain will be impacted by the deletion of a data domain object.
301
+ * @param key The key of the impacted data domain object.
302
+ * @param kind The kind of the impacted data object.
303
+ * @returns The delete impact analysis report.
304
+ */
305
+ deleteAnalysis(key: string, kind: ImpactKinds): ImpactReport {
306
+ this.report = {
307
+ key,
308
+ kind,
309
+ impact: [],
310
+ canProceed: true,
311
+ }
312
+ this.report.impact = this.createDeleteImpact(key, kind, key)
313
+ // this.report.impact.unshift({
314
+ // key,
315
+ // kind,
316
+ // type: 'delete',
317
+ // impact: `The ${this.kindToLabel(kind)} with key ${key} will be deleted.`,
318
+ // blocking: false,
319
+ // })
320
+ return this.report
321
+ }
322
+
323
+ protected createDeleteImpact(key: string, kind: ImpactKinds, rootKey: string): ImpactItem[] {
324
+ switch (kind) {
325
+ case DataNamespaceKind:
326
+ return this.deleteNamespaceAnalysis(key, rootKey)
327
+ case DataModelKind:
328
+ return this.deleteDataModelAnalysis(key, rootKey)
329
+ case DataEntityKind:
330
+ return this.deleteEntityAnalysis(key, rootKey)
331
+ case DataPropertyKind:
332
+ return this.deletePropertyAnalysis(key)
333
+ case DataAssociationKind:
334
+ return this.deleteAssociationAnalysis(key)
335
+ default:
336
+ return []
337
+ }
338
+ }
339
+
340
+ protected deleteNamespaceAnalysis(key: string, rootKey: string): ImpactItem[] {
341
+ const result: ImpactItem[] = []
342
+ const ns = this.root.findNamespace(key)
343
+ if (!ns) {
344
+ return result
345
+ }
346
+ result.push({
347
+ key: ns.key,
348
+ kind: ns.kind,
349
+ type: 'delete',
350
+ impact: `The ${ns.info.renderLabel} ${this.kindToLabel(DataNamespaceKind)} will be deleted.`,
351
+ blocking: false,
352
+ })
353
+ const namespaces = ns.listNamespaces()
354
+ namespaces.forEach((child) => {
355
+ const items = this.deleteNamespaceAnalysis(child.key, rootKey)
356
+ result.push(...items)
357
+ })
358
+ const models = ns.listDataModels()
359
+ models.forEach((child) => {
360
+ const items = this.deleteDataModelAnalysis(child.key, rootKey)
361
+ result.push(...items)
362
+ })
363
+ return result
364
+ }
365
+
366
+ protected deleteDataModelAnalysis(key: string, rootKey: string): ImpactItem[] {
367
+ const result: ImpactItem[] = []
368
+ const model = this.root.findDataModel(key)
369
+ if (!model) {
370
+ return result
371
+ }
372
+ result.push({
373
+ key: model.key,
374
+ kind: model.kind,
375
+ type: 'delete',
376
+ impact: `The ${model.info.renderLabel} ${this.kindToLabel(DataModelKind)} will be deleted.`,
377
+ blocking: false,
378
+ })
379
+ model.entities.forEach((child) => {
380
+ const items = this.deleteEntityAnalysis(child.key, rootKey)
381
+ result.push(...items)
382
+ })
383
+ return result
384
+ }
385
+
386
+ protected deleteEntityAnalysis(key: string, rootKey: string): ImpactItem[] {
387
+ const result: ImpactItem[] = []
388
+ const entity = this.root.findEntity(key)
389
+ if (!entity) {
390
+ return result
391
+ }
392
+ result.push({
393
+ key: entity.key,
394
+ kind: entity.kind,
395
+ type: 'delete',
396
+ impact: `The ${entity.info.renderLabel} ${this.kindToLabel(DataEntityKind)} will be deleted.`,
397
+ blocking: false,
398
+ })
399
+ // We need to know whether the entity is a parent of another entity
400
+ const children = this.root.definitions.entities.filter((domainEntity) => {
401
+ if (domainEntity.key === entity.key) {
402
+ // ignore self
403
+ return false
404
+ }
405
+ if (!domainEntity.parents.includes(entity.key)) {
406
+ // no relationship
407
+ return false
408
+ }
409
+ if (domainEntity.isChildOf(rootKey)) {
410
+ // No need to include this parent as it itself is being deleted
411
+ return false
412
+ }
413
+ // the entity has a parent-child relationship to the deleted entity.
414
+ return true
415
+ })
416
+ if (children.length) {
417
+ children.forEach((child) => {
418
+ result.push({
419
+ key: child.key,
420
+ kind: child.kind,
421
+ type: 'delete',
422
+ impact: `The ${child.info.renderLabel} ${this.kindToLabel(DataEntityKind)} will become an orphan because it is a child of ${entity.info.renderLabel}.`,
423
+ resolution: `The ${entity.info.renderLabel} will be removed as the parent parent of ${child.info.renderLabel}.`,
424
+ blocking: true,
425
+ relationship: 'child',
426
+ })
427
+ })
428
+ this.report.canProceed = false
429
+ }
430
+ // We need to know whether there's another entity that has an association to this entity.
431
+ const inAssociations = this.root.definitions.associations.filter((association) => {
432
+ return association.targets.some((item) => {
433
+ if (item.key === entity.key) {
434
+ const related = association.getParentInstance()
435
+ if (!related) {
436
+ return false
437
+ }
438
+ if (related.isChildOf(rootKey)) {
439
+ // No need to include this association as the related entity itself is being deleted
440
+ return false
441
+ }
442
+ return true
443
+ }
444
+ return false
445
+ })
446
+ })
447
+ if (inAssociations.length) {
448
+ inAssociations.forEach((association) => {
449
+ result.push({
450
+ key: association.key,
451
+ kind: association.kind,
452
+ type: 'delete',
453
+ impact: `The ${association.info.renderLabel} ${this.kindToLabel(DataAssociationKind)} will be broken because it has a target to ${entity.info.renderLabel}.`,
454
+ resolution: `The ${association.info.renderLabel} ${this.kindToLabel(DataAssociationKind)} will be removed from ${entity.info.renderLabel}.`,
455
+ blocking: true,
456
+ })
457
+ })
458
+ this.report.canProceed = false
459
+ }
460
+ entity.properties.forEach((child) => {
461
+ const items = this.deletePropertyAnalysis(child.key)
462
+ result.push(...items)
463
+ })
464
+ entity.associations.forEach((child) => {
465
+ const items = this.deleteAssociationAnalysis(child.key)
466
+ result.push(...items)
467
+ })
468
+ return result
469
+ }
470
+
471
+ protected deletePropertyAnalysis(key: string): ImpactItem[] {
472
+ const result: ImpactItem[] = []
473
+ const property = this.root.findProperty(key)
474
+ if (!property) {
475
+ return result
476
+ }
477
+ result.push({
478
+ key: property.key,
479
+ kind: property.kind,
480
+ type: 'delete',
481
+ impact: `The ${property.info.renderLabel} ${this.kindToLabel(DataPropertyKind)} will be deleted.`,
482
+ blocking: false,
483
+ })
484
+ return result
485
+ }
486
+
487
+ protected deleteAssociationAnalysis(key: string): ImpactItem[] {
488
+ const result: ImpactItem[] = []
489
+ const association = this.root.findAssociation(key)
490
+ if (!association) {
491
+ return result
492
+ }
493
+ result.push({
494
+ key: association.key,
495
+ kind: association.kind,
496
+ type: 'delete',
497
+ impact: `The ${association.info.renderLabel} ${this.kindToLabel(DataAssociationKind)} will be deleted.`,
498
+ blocking: false,
499
+ })
500
+ return result
501
+ }
502
+
503
+ protected kindToLabel(kind: ImpactKinds): string {
504
+ switch (kind) {
505
+ case DataNamespaceKind:
506
+ return 'namespace'
507
+ case DataEntityKind:
508
+ return 'entity'
509
+ case DataModelKind:
510
+ return 'data model'
511
+ case DataPropertyKind:
512
+ return 'property'
513
+ case DataAssociationKind:
514
+ return 'association'
515
+ default:
516
+ return 'unknown'
517
+ }
518
+ }
519
+ }
@@ -0,0 +1,13 @@
1
+ export interface DataDomainRemoveOptions {
2
+ /**
3
+ * When true, the object will be forcebly removed.
4
+ * The resolution defined in the `ImpactResolution` class will be applied.
5
+ *
6
+ * For example, when removing an entity that is a parent to another entity, it will
7
+ * removed itself as a parent from the child entity.
8
+ *
9
+ * Note, this option should only be used when the ImpactAnalysis has been performed
10
+ * and the user was informed of the impact.
11
+ */
12
+ force?: boolean
13
+ }
@@ -24,6 +24,7 @@ export class ExpressServer {
24
24
  app.disable('etag')
25
25
  app.disable('x-powered-by')
26
26
  app.set('trust proxy', true)
27
+ app.set('query parser', 'extended')
27
28
  this.setupRoutes()
28
29
  }
29
30
 
@@ -19,7 +19,7 @@ export class BaseApi {
19
19
  * @param router Express app.
20
20
  */
21
21
  setCors(router: Router): void {
22
- router.options('*', cors(this._processCors))
22
+ router.options('*splat', cors(this._processCors))
23
23
  }
24
24
 
25
25
  /**
@@ -43,5 +43,5 @@ class TestApiRoute extends BaseApi {
43
43
  const api = new TestApiRoute()
44
44
  api.setCors(router)
45
45
  const checkCorsFn = api._processCors
46
- router.post('/', cors(checkCorsFn), api.createTest.bind(api))
47
46
  router.get('/', cors(checkCorsFn), api.listTest.bind(api))
47
+ router.post('/', cors(checkCorsFn), api.createTest.bind(api))
@@ -643,3 +643,76 @@ test.group('DataAssociation Multiple targets', (g) => {
643
643
  assert.deepEqual(a1.targets, [{ key: e2.key }, { key: e3.key }])
644
644
  })
645
645
  })
646
+
647
+ test.group('DataAssociation.isChildOf()', (group) => {
648
+ let root: DataNamespace
649
+ let n1: DataNamespace
650
+ let n2: DataNamespace
651
+ let m1: DataModel
652
+ let m2: DataModel
653
+ let e1: DataEntity
654
+ let e2: DataEntity
655
+ let a1: DataAssociation
656
+ let a2: DataAssociation
657
+ let a3: DataAssociation
658
+
659
+ group.each.setup(() => {
660
+ root = new DataNamespace()
661
+ n1 = root.addNamespace('n1')
662
+ n2 = n1.addNamespace('n2')
663
+ m1 = root.addDataModel('m1')
664
+ m2 = n1.addDataModel('m2')
665
+ e1 = m1.addEntity('e1')
666
+ e2 = m2.addEntity('e2')
667
+ a1 = e1.addNamedAssociation('a1')
668
+ a2 = e2.addNamedAssociation('a2')
669
+ a3 = n2.addDataModel('m3').addEntity('e3').addNamedAssociation('a3')
670
+ })
671
+
672
+ test('returns false when called on an association not in an entity', ({ assert }) => {
673
+ const a4 = new DataAssociation(root)
674
+ const result = a4.isChildOf('some-key')
675
+ assert.isFalse(result)
676
+ })
677
+
678
+ test('returns false when called on self', ({ assert }) => {
679
+ const result = a2.isChildOf(a2.key)
680
+ assert.isFalse(result)
681
+ })
682
+
683
+ test('returns true when called on a direct parent', ({ assert }) => {
684
+ const result = a2.isChildOf(e2.key)
685
+ assert.isTrue(result)
686
+ })
687
+
688
+ test('returns true when called on a grandparent', ({ assert }) => {
689
+ const result = a3.isChildOf(root.key)
690
+ assert.isTrue(result)
691
+ })
692
+
693
+ test('returns false when called on a sibling', ({ assert }) => {
694
+ const a4 = e1.addNamedAssociation('a4')
695
+ const result = a1.isChildOf(a4.key)
696
+ assert.isFalse(result)
697
+ })
698
+
699
+ test('returns false when called on a child', ({ assert }) => {
700
+ const result = e2.isChildOf(a2.key)
701
+ assert.isFalse(result)
702
+ })
703
+
704
+ test('returns false when called on a non-existent namespace', ({ assert }) => {
705
+ const result = a2.isChildOf('non-existent-key')
706
+ assert.isFalse(result)
707
+ })
708
+
709
+ test('returns true when called on a grandparent', ({ assert }) => {
710
+ const result = a3.isChildOf(n1.key)
711
+ assert.isTrue(result)
712
+ })
713
+
714
+ test('returns true when called on a data model in the same namespace', ({ assert }) => {
715
+ const result = a1.isChildOf(m1.key)
716
+ assert.isTrue(result)
717
+ })
718
+ })