@api-client/core 0.19.9 → 0.19.10

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 (98) hide show
  1. package/Testing.md +1 -1
  2. package/build/src/decorators/observed.d.ts.map +1 -1
  3. package/build/src/decorators/observed.js +91 -0
  4. package/build/src/decorators/observed.js.map +1 -1
  5. package/build/src/modeling/ApiModel.d.ts +21 -7
  6. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  7. package/build/src/modeling/ApiModel.js +70 -29
  8. package/build/src/modeling/ApiModel.js.map +1 -1
  9. package/build/src/modeling/DomainValidation.d.ts +1 -1
  10. package/build/src/modeling/DomainValidation.d.ts.map +1 -1
  11. package/build/src/modeling/DomainValidation.js.map +1 -1
  12. package/build/src/modeling/ExposedEntity.d.ts +14 -0
  13. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  14. package/build/src/modeling/ExposedEntity.js +59 -6
  15. package/build/src/modeling/ExposedEntity.js.map +1 -1
  16. package/build/src/modeling/actions/Action.d.ts +11 -1
  17. package/build/src/modeling/actions/Action.d.ts.map +1 -1
  18. package/build/src/modeling/actions/Action.js +21 -3
  19. package/build/src/modeling/actions/Action.js.map +1 -1
  20. package/build/src/modeling/actions/CreateAction.d.ts +2 -1
  21. package/build/src/modeling/actions/CreateAction.d.ts.map +1 -1
  22. package/build/src/modeling/actions/CreateAction.js +2 -2
  23. package/build/src/modeling/actions/CreateAction.js.map +1 -1
  24. package/build/src/modeling/actions/DeleteAction.d.ts +2 -1
  25. package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -1
  26. package/build/src/modeling/actions/DeleteAction.js +2 -2
  27. package/build/src/modeling/actions/DeleteAction.js.map +1 -1
  28. package/build/src/modeling/actions/ListAction.d.ts +2 -1
  29. package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
  30. package/build/src/modeling/actions/ListAction.js +2 -2
  31. package/build/src/modeling/actions/ListAction.js.map +1 -1
  32. package/build/src/modeling/actions/ReadAction.d.ts +2 -1
  33. package/build/src/modeling/actions/ReadAction.d.ts.map +1 -1
  34. package/build/src/modeling/actions/ReadAction.js +2 -2
  35. package/build/src/modeling/actions/ReadAction.js.map +1 -1
  36. package/build/src/modeling/actions/SearchAction.d.ts +2 -1
  37. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
  38. package/build/src/modeling/actions/SearchAction.js +2 -2
  39. package/build/src/modeling/actions/SearchAction.js.map +1 -1
  40. package/build/src/modeling/actions/UpdateAction.d.ts +2 -1
  41. package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -1
  42. package/build/src/modeling/actions/UpdateAction.js +2 -2
  43. package/build/src/modeling/actions/UpdateAction.js.map +1 -1
  44. package/build/src/modeling/actions/index.d.ts +2 -1
  45. package/build/src/modeling/actions/index.d.ts.map +1 -1
  46. package/build/src/modeling/actions/index.js +7 -7
  47. package/build/src/modeling/actions/index.js.map +1 -1
  48. package/build/src/modeling/index.d.ts +1 -0
  49. package/build/src/modeling/index.d.ts.map +1 -1
  50. package/build/src/modeling/index.js +1 -0
  51. package/build/src/modeling/index.js.map +1 -1
  52. package/build/src/modeling/types.d.ts +67 -0
  53. package/build/src/modeling/types.d.ts.map +1 -1
  54. package/build/src/modeling/types.js.map +1 -1
  55. package/build/src/modeling/validation/api_model_rules.d.ts +15 -0
  56. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -0
  57. package/build/src/modeling/validation/api_model_rules.js +599 -0
  58. package/build/src/modeling/validation/api_model_rules.js.map +1 -0
  59. package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
  60. package/build/src/modeling/validation/association_validation.js +1 -3
  61. package/build/src/modeling/validation/association_validation.js.map +1 -1
  62. package/build/tsconfig.tsbuildinfo +1 -1
  63. package/data/models/example-generator-api.json +8 -8
  64. package/eslint.config.js +0 -1
  65. package/package.json +17 -122
  66. package/src/decorators/observed.ts +91 -0
  67. package/src/modeling/ApiModel.ts +73 -33
  68. package/src/modeling/DomainValidation.ts +1 -1
  69. package/src/modeling/ExposedEntity.ts +63 -9
  70. package/src/modeling/actions/Action.ts +25 -2
  71. package/src/modeling/actions/CreateAction.ts +3 -2
  72. package/src/modeling/actions/DeleteAction.ts +3 -2
  73. package/src/modeling/actions/ListAction.ts +3 -2
  74. package/src/modeling/actions/ReadAction.ts +3 -2
  75. package/src/modeling/actions/SearchAction.ts +3 -2
  76. package/src/modeling/actions/UpdateAction.ts +3 -2
  77. package/src/modeling/types.ts +70 -0
  78. package/src/modeling/validation/api_model_rules.ts +640 -0
  79. package/src/modeling/validation/api_model_validation_rules.md +58 -0
  80. package/src/modeling/validation/association_validation.ts +1 -3
  81. package/tests/unit/modeling/actions/Action.spec.ts +40 -8
  82. package/tests/unit/modeling/actions/CreateAction.spec.ts +5 -5
  83. package/tests/unit/modeling/actions/DeleteAction.spec.ts +6 -6
  84. package/tests/unit/modeling/actions/ListAction.spec.ts +7 -7
  85. package/tests/unit/modeling/actions/ReadAction.spec.ts +6 -6
  86. package/tests/unit/modeling/actions/SearchAction.spec.ts +6 -6
  87. package/tests/unit/modeling/actions/UpdateAction.spec.ts +6 -6
  88. package/tests/unit/modeling/api_model.spec.ts +190 -13
  89. package/tests/unit/modeling/api_model_expose_entity.spec.ts +43 -19
  90. package/tests/unit/modeling/api_model_remove_entity.spec.ts +6 -6
  91. package/tests/unit/modeling/exposed_entity.spec.ts +123 -3
  92. package/tests/unit/modeling/exposed_entity_actions.spec.ts +41 -18
  93. package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +1 -1
  94. package/tests/unit/modeling/rules/restoring_rules.spec.ts +9 -5
  95. package/tests/unit/modeling/validation/api_model_rules.spec.ts +324 -0
  96. package/tsconfig.browser.json +1 -1
  97. package/tsconfig.node.json +1 -1
  98. package/bin/test-web.ts +0 -6
@@ -6,7 +6,7 @@ import { restoreAction } from './actions/index.js'
6
6
  import type { ApiModel } from './ApiModel.js'
7
7
  import { ensureLeadingSlash, joinPaths } from './helpers/endpointHelpers.js'
8
8
  import { AccessRule } from './rules/AccessRule.js'
9
- import { restoreAccessRule } from './rules/index.js'
9
+ import { type RateLimitRule, restoreAccessRule } from './rules/index.js'
10
10
  import { RateLimitingConfiguration } from './rules/RateLimitingConfiguration.js'
11
11
  import type { AssociationTarget, ExposeOptions, ExposeParentRef, ExposedEntitySchema } from './types.js'
12
12
 
@@ -98,7 +98,7 @@ export class ExposedEntity extends EventTarget {
98
98
  /**
99
99
  * Optional configuration for rate limiting for this exposure.
100
100
  */
101
- @observed({ deep: true }) accessor rateLimiting: RateLimitingConfiguration | undefined
101
+ @observed() accessor rateLimiting: RateLimitingConfiguration | undefined
102
102
 
103
103
  /**
104
104
  * When true, generation for this exposure hit configured limits
@@ -184,7 +184,7 @@ export class ExposedEntity extends EventTarget {
184
184
  this.isRoot = init.isRoot
185
185
  this.parent = init.parent
186
186
  this.exposeOptions = init.exposeOptions
187
- this.actions = init.actions ? init.actions.map((a) => restoreAction(a)) : []
187
+ this.actions = init.actions ? init.actions.map((a) => restoreAction(this, a)) : []
188
188
  this.accessRule = init.accessRule ? init.accessRule.map((ar) => restoreAccessRule(ar)) : []
189
189
  this.rateLimiting = init.rateLimiting ? new RateLimitingConfiguration(init.rateLimiting) : undefined
190
190
  this.truncated = init.truncated
@@ -259,9 +259,7 @@ export class ExposedEntity extends EventTarget {
259
259
 
260
260
  // Check for collision if this is a root entity
261
261
  if (this.isRoot) {
262
- const collision = this.api.exposes.find(
263
- (e) => e.isRoot && e.key !== this.key && e.collectionPath === normalizedCollection
264
- )
262
+ const collision = this.api.findCollectionPathCollision(normalizedCollection, this.key)
265
263
  if (collision) {
266
264
  throw new Error(`Collection path "${normalizedCollection}" is already in use by another root entity.`)
267
265
  }
@@ -328,7 +326,7 @@ export class ExposedEntity extends EventTarget {
328
326
  }
329
327
  // Check for collision if this is a root entity (singleton case)
330
328
  if (this.isRoot) {
331
- const collision = this.api.exposes.find((e) => e.isRoot && e.key !== this.key && e.resourcePath === cleaned)
329
+ const collision = this.api.findResourcePathCollision(cleaned, this.key)
332
330
  if (collision) {
333
331
  throw new Error(`Resource path "${cleaned}" is already in use by another root entity.`)
334
332
  }
@@ -350,7 +348,7 @@ export class ExposedEntity extends EventTarget {
350
348
  // Traverse parents, always joining with the parent's resource path
351
349
  let parentKey = this.parent?.key
352
350
  while (parentKey) {
353
- const parent = this.api.exposes.find((e) => e.key === parentKey)
351
+ const parent = this.api.exposes.get(parentKey)
354
352
  if (!parent) break
355
353
  const parentResource = ensureLeadingSlash(parent.resourcePath)
356
354
  absolute = joinPaths(parentResource, absolute)
@@ -372,7 +370,7 @@ export class ExposedEntity extends EventTarget {
372
370
  // Traverse parents, always joining with the parent's resource path
373
371
  let parentKey = this.parent?.key
374
372
  while (parentKey) {
375
- const parent = this.api.exposes.find((e) => e.key === parentKey)
373
+ const parent = this.api.exposes.get(parentKey)
376
374
  if (!parent) break
377
375
  const parentResource = ensureLeadingSlash(parent.resourcePath)
378
376
  absolute = joinPaths(parentResource, absolute)
@@ -380,4 +378,60 @@ export class ExposedEntity extends EventTarget {
380
378
  }
381
379
  return absolute
382
380
  }
381
+
382
+ /**
383
+ * Returns all access rules for this exposure, including the ones from all the parents up to the API.
384
+ */
385
+ getAllRules(): AccessRule[] {
386
+ const rules: AccessRule[] = []
387
+ this.api.accessRule.forEach((rule) => rules.push(rule))
388
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
389
+ let current: ExposedEntity | undefined = this
390
+ while (current) {
391
+ if (current.accessRule) {
392
+ rules.push(...current.accessRule)
393
+ }
394
+ current = current.parent ? this.api.exposes.get(current.parent.key) : undefined
395
+ }
396
+ return rules
397
+ }
398
+
399
+ /**
400
+ * Returns all rate limiters for this exposure, including the ones from all the parents up to the API.
401
+ */
402
+ getAllRateLimiters(): RateLimitingConfiguration[] {
403
+ const rateLimiters: RateLimitingConfiguration[] = []
404
+ if (this.api.rateLimiting) {
405
+ rateLimiters.push(this.api.rateLimiting)
406
+ }
407
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
408
+ let current: ExposedEntity | undefined = this
409
+ while (current) {
410
+ if (current.rateLimiting) {
411
+ rateLimiters.push(current.rateLimiting)
412
+ }
413
+ current = current.parent ? this.api.exposes.get(current.parent.key) : undefined
414
+ }
415
+ return rateLimiters
416
+ }
417
+
418
+ /**
419
+ * Returns all rate limiter rules for this exposure, including the ones from all the parents up to the API.
420
+ * Similar to the getAllRules() method, but it flattens the rate limiters into a single list of rules.
421
+ */
422
+ getAllRateLimiterRules(): RateLimitRule[] {
423
+ const rules: RateLimitRule[] = []
424
+ if (this.api.rateLimiting) {
425
+ rules.push(...this.api.rateLimiting.rules)
426
+ }
427
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
428
+ let current: ExposedEntity | undefined = this
429
+ while (current) {
430
+ if (current.rateLimiting) {
431
+ rules.push(...current.rateLimiting.rules)
432
+ }
433
+ current = current.parent ? this.api.exposes.get(current.parent.key) : undefined
434
+ }
435
+ return rules
436
+ }
383
437
  }
@@ -2,6 +2,7 @@ import { AccessRule, AccessRuleSchema } from '../rules/AccessRule.js'
2
2
  import { RateLimitingConfiguration, RateLimitingConfigurationSchema } from '../rules/RateLimitingConfiguration.js'
3
3
  import { observed } from '../../decorators/observed.js'
4
4
  import { restoreAccessRule } from '../rules/index.js'
5
+ import type { ExposedEntity } from '../ExposedEntity.js'
5
6
 
6
7
  /**
7
8
  * A base interface for common properties across all actions.
@@ -37,10 +38,13 @@ export interface ActionSchema {
37
38
  export class Action extends EventTarget implements ActionSchema {
38
39
  @observed() accessor kind: string
39
40
  @observed({ deep: true }) accessor accessRule: AccessRule[]
40
- @observed({ deep: true }) accessor rateLimiting: RateLimitingConfiguration | undefined
41
+ @observed() accessor rateLimiting: RateLimitingConfiguration | undefined
41
42
 
42
- constructor(state: Partial<ActionSchema> = {}) {
43
+ protected parent: ExposedEntity
44
+
45
+ constructor(parent: ExposedEntity, state: Partial<ActionSchema> = {}) {
43
46
  super()
47
+ this.parent = parent
44
48
  this.kind = state.kind || ''
45
49
  this.accessRule = state.accessRule ? state.accessRule.map((rule) => restoreAccessRule(rule)) : []
46
50
  this.rateLimiting = state.rateLimiting ? new RateLimitingConfiguration(state.rateLimiting) : undefined
@@ -62,4 +66,23 @@ export class Action extends EventTarget implements ActionSchema {
62
66
  }
63
67
  return result
64
68
  }
69
+
70
+ /**
71
+ * Returns all access rules for this action, including the ones from all the parents up to the API.
72
+ */
73
+ getAllRules(): AccessRule[] {
74
+ const rules: AccessRule[] = [...this.parent.getAllRules(), ...this.accessRule]
75
+ return rules
76
+ }
77
+
78
+ /**
79
+ * Returns all rate limiters for this action, including the ones from all the parents up to the API.
80
+ */
81
+ getAllRateLimiters(): RateLimitingConfiguration[] {
82
+ const rateLimiters = this.parent.getAllRateLimiters()
83
+ if (this.rateLimiting) {
84
+ rateLimiters.push(this.rateLimiting)
85
+ }
86
+ return rateLimiters
87
+ }
65
88
  }
@@ -1,5 +1,6 @@
1
1
  import { Action, type ActionSchema } from './Action.js'
2
2
  import { observed } from '../../decorators/observed.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
3
4
 
4
5
  /**
5
6
  * Enables adding a new resource to a collection.
@@ -16,8 +17,8 @@ export interface CreateActionSchema extends ActionSchema {
16
17
  export class CreateAction extends Action implements CreateActionSchema {
17
18
  @observed() override accessor kind: 'create'
18
19
 
19
- constructor(state: Partial<CreateActionSchema> = {}) {
20
- super(state)
20
+ constructor(parent: ExposedEntity, state: Partial<CreateActionSchema> = {}) {
21
+ super(parent, state)
21
22
  this.kind = 'create'
22
23
  }
23
24
 
@@ -1,5 +1,6 @@
1
1
  import { Action, type ActionSchema } from './Action.js'
2
2
  import { observed } from '../../decorators/observed.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
3
4
 
4
5
  export type DeleteStrategy = 'soft' | 'hard'
5
6
 
@@ -33,8 +34,8 @@ export class DeleteAction extends Action implements DeleteActionSchema {
33
34
  @observed() accessor strategy: DeleteStrategy
34
35
  @observed() accessor retentionPeriod: number
35
36
 
36
- constructor(state: Partial<DeleteActionSchema> = {}) {
37
- super(state)
37
+ constructor(parent: ExposedEntity, state: Partial<DeleteActionSchema> = {}) {
38
+ super(parent, state)
38
39
  this.kind = 'delete'
39
40
  this.strategy = state.strategy || 'soft'
40
41
  this.retentionPeriod = state.retentionPeriod ?? 30
@@ -1,6 +1,7 @@
1
1
  import type { PaginationStrategy } from '../types.js'
2
2
  import { Action, type ActionSchema } from './Action.js'
3
3
  import { observed, toRaw } from '../../decorators/observed.js'
4
+ import type { ExposedEntity } from '../ExposedEntity.js'
4
5
 
5
6
  /**
6
7
  * Enables retrieving a collection of resources.
@@ -35,8 +36,8 @@ export class ListAction extends Action implements ListActionSchema {
35
36
  @observed({ deep: true }) accessor filterableFields: string[] = []
36
37
  @observed({ deep: true }) accessor sortableFields: string[] = []
37
38
 
38
- constructor(state: Partial<ListActionSchema> = {}) {
39
- super(state)
39
+ constructor(parent: ExposedEntity, state: Partial<ListActionSchema> = {}) {
40
+ super(parent, state)
40
41
  this.kind = state.kind || 'list'
41
42
  this.pagination = state.pagination
42
43
  ? { ...state.pagination }
@@ -1,5 +1,6 @@
1
1
  import { Action, type ActionSchema } from './Action.js'
2
2
  import { observed } from '../../decorators/observed.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
3
4
 
4
5
  /**
5
6
  * Enables retrieving a single resource by its ID.
@@ -18,8 +19,8 @@ export interface ReadActionSchema extends ActionSchema {
18
19
  export class ReadAction extends Action implements ReadActionSchema {
19
20
  @observed() override accessor kind: 'read'
20
21
 
21
- constructor(state: Partial<ReadActionSchema> = {}) {
22
- super(state)
22
+ constructor(parent: ExposedEntity, state: Partial<ReadActionSchema> = {}) {
23
+ super(parent, state)
23
24
  this.kind = 'read'
24
25
  }
25
26
 
@@ -1,5 +1,6 @@
1
1
  import { Action, type ActionSchema } from './Action.js'
2
2
  import { observed, toRaw } from '../../decorators/observed.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
3
4
 
4
5
  /**
5
6
  * Enables keyword-based search across specified fields.
@@ -22,8 +23,8 @@ export class SearchAction extends Action implements SearchActionSchema {
22
23
  @observed() override accessor kind: 'search'
23
24
  @observed({ deep: true }) accessor fields: string[]
24
25
 
25
- constructor(state: Partial<SearchActionSchema> = {}) {
26
- super(state)
26
+ constructor(parent: ExposedEntity, state: Partial<SearchActionSchema> = {}) {
27
+ super(parent, state)
27
28
  this.kind = 'search'
28
29
  this.fields = state.fields ? [...state.fields] : []
29
30
  }
@@ -1,5 +1,6 @@
1
1
  import { Action, type ActionSchema } from './Action.js'
2
2
  import { observed, toRaw } from '../../decorators/observed.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
3
4
 
4
5
  /**
5
6
  * Enables modifying an existing resource.
@@ -25,8 +26,8 @@ export class UpdateAction extends Action implements UpdateActionSchema {
25
26
  @observed() override accessor kind: 'update'
26
27
  @observed({ deep: true }) accessor allowedMethods: ('PUT' | 'PATCH')[]
27
28
 
28
- constructor(state: Partial<UpdateActionSchema> = {}) {
29
- super(state)
29
+ constructor(parent: ExposedEntity, state: Partial<UpdateActionSchema> = {}) {
30
+ super(parent, state)
30
31
  this.kind = 'update'
31
32
  this.allowedMethods = state.allowedMethods ? [...state.allowedMethods] : ['PATCH']
32
33
  }
@@ -769,3 +769,73 @@ export interface DomainSearchResult {
769
769
  */
770
770
  isForeign: boolean
771
771
  }
772
+
773
+ /**
774
+ * Contextual information about where the API Model validation issue occurred.
775
+ * This allows the UI to trace back a validation error to a specific module or component.
776
+ */
777
+ export interface ApiModelValidationContext {
778
+ /**
779
+ * The key of the ApiModel itself.
780
+ */
781
+ apiModelKey: string
782
+ /**
783
+ * The kind of the affected component (e.g., 'ApiModel', 'ExposedEntity', 'Action').
784
+ */
785
+ kind: string
786
+ /**
787
+ * The key of the specific entity or action where the issue occurred.
788
+ * If the error is at the root API model, this might be the ApiModel's key.
789
+ */
790
+ key: string
791
+ /**
792
+ * The specific property being validated, if applicable.
793
+ */
794
+ property?: string
795
+ /**
796
+ * The key of the parent ExposedEntity, if this issue occurred on a nested entity or an action.
797
+ */
798
+ parentExposedEntityKey?: string
799
+ }
800
+
801
+ /**
802
+ * Represents a single validation issue found within an ApiModel.
803
+ */
804
+ export interface ApiModelValidationItem {
805
+ /**
806
+ * A unique code identifying the type of validation error. This enables
807
+ * programmatic handling or auto-fixing in the UI (e.g., 'API_MISSING_NAME', 'LIST_ACTION_MISSING_PAGINATION').
808
+ */
809
+ code: string
810
+ /**
811
+ * A human-readable description of the problem.
812
+ */
813
+ message: string
814
+ /**
815
+ * A human-readable suggestion on how to fix the issue.
816
+ */
817
+ suggestion: string
818
+ /**
819
+ * The severity of the validation issue.
820
+ */
821
+ severity: 'error' | 'warning' | 'info'
822
+ /**
823
+ * Contextual information to trace the issue back to its source component.
824
+ */
825
+ context: ApiModelValidationContext
826
+ }
827
+
828
+ /**
829
+ * The consolidated result of validating an entire ApiModel or a subset of it.
830
+ */
831
+ export interface ApiModelValidationResult {
832
+ /**
833
+ * A boolean indicating whether the model is structurally sound and ready for publication.
834
+ * Typically true if there are no 'error' severity issues.
835
+ */
836
+ isValid: boolean
837
+ /**
838
+ * The aggregated list of all validation issues found.
839
+ */
840
+ issues: ApiModelValidationItem[]
841
+ }