@api-client/core 0.19.22 → 0.19.23

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 (72) hide show
  1. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  2. package/build/src/modeling/ApiModel.js +37 -13
  3. package/build/src/modeling/ApiModel.js.map +1 -1
  4. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  5. package/build/src/modeling/ExposedEntity.js +54 -15
  6. package/build/src/modeling/ExposedEntity.js.map +1 -1
  7. package/build/src/modeling/actions/Action.js +2 -2
  8. package/build/src/modeling/actions/Action.js.map +1 -1
  9. package/build/src/modeling/rules/AccessRule.d.ts +5 -1
  10. package/build/src/modeling/rules/AccessRule.d.ts.map +1 -1
  11. package/build/src/modeling/rules/AccessRule.js +4 -1
  12. package/build/src/modeling/rules/AccessRule.js.map +1 -1
  13. package/build/src/modeling/rules/AllowAuthenticated.d.ts +4 -1
  14. package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -1
  15. package/build/src/modeling/rules/AllowAuthenticated.js +2 -2
  16. package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -1
  17. package/build/src/modeling/rules/AllowPublic.d.ts +4 -1
  18. package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -1
  19. package/build/src/modeling/rules/AllowPublic.js +2 -2
  20. package/build/src/modeling/rules/AllowPublic.js.map +1 -1
  21. package/build/src/modeling/rules/MatchEmailDomain.d.ts +4 -1
  22. package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -1
  23. package/build/src/modeling/rules/MatchEmailDomain.js +2 -2
  24. package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -1
  25. package/build/src/modeling/rules/MatchResourceOwner.d.ts +4 -1
  26. package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -1
  27. package/build/src/modeling/rules/MatchResourceOwner.js +2 -2
  28. package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -1
  29. package/build/src/modeling/rules/MatchUserProperty.d.ts +4 -1
  30. package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -1
  31. package/build/src/modeling/rules/MatchUserProperty.js +2 -2
  32. package/build/src/modeling/rules/MatchUserProperty.js.map +1 -1
  33. package/build/src/modeling/rules/MatchUserRole.d.ts +4 -1
  34. package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -1
  35. package/build/src/modeling/rules/MatchUserRole.js +2 -2
  36. package/build/src/modeling/rules/MatchUserRole.js.map +1 -1
  37. package/build/src/modeling/rules/index.d.ts +4 -1
  38. package/build/src/modeling/rules/index.d.ts.map +1 -1
  39. package/build/src/modeling/rules/index.js +7 -7
  40. package/build/src/modeling/rules/index.js.map +1 -1
  41. package/build/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +1 -1
  43. package/src/modeling/ApiModel.ts +37 -13
  44. package/src/modeling/ExposedEntity.ts +62 -16
  45. package/src/modeling/actions/Action.ts +2 -2
  46. package/src/modeling/rules/AccessRule.ts +8 -1
  47. package/src/modeling/rules/AllowAuthenticated.ts +5 -2
  48. package/src/modeling/rules/AllowPublic.ts +5 -2
  49. package/src/modeling/rules/MatchEmailDomain.ts +5 -2
  50. package/src/modeling/rules/MatchResourceOwner.ts +5 -2
  51. package/src/modeling/rules/MatchUserProperty.ts +5 -2
  52. package/src/modeling/rules/MatchUserRole.ts +5 -2
  53. package/tests/unit/modeling/actions/Action.spec.ts +13 -10
  54. package/tests/unit/modeling/actions/CreateAction.spec.ts +7 -6
  55. package/tests/unit/modeling/actions/DeleteAction.spec.ts +7 -6
  56. package/tests/unit/modeling/actions/ListAction.spec.ts +5 -4
  57. package/tests/unit/modeling/actions/ReadAction.spec.ts +9 -8
  58. package/tests/unit/modeling/actions/SearchAction.spec.ts +5 -4
  59. package/tests/unit/modeling/actions/UpdateAction.spec.ts +7 -6
  60. package/tests/unit/modeling/actions/helpers.ts +7 -0
  61. package/tests/unit/modeling/api_model.spec.ts +3 -1
  62. package/tests/unit/modeling/api_model_expose_entity.spec.ts +5 -17
  63. package/tests/unit/modeling/exposed_entity.spec.ts +6 -2
  64. package/tests/unit/modeling/exposed_entity_actions.spec.ts +0 -4
  65. package/tests/unit/modeling/rules/AccessRule.spec.ts +6 -5
  66. package/tests/unit/modeling/rules/AllowAuthenticated.spec.ts +4 -3
  67. package/tests/unit/modeling/rules/AllowPublic.spec.ts +4 -3
  68. package/tests/unit/modeling/rules/MatchEmailDomain.spec.ts +6 -5
  69. package/tests/unit/modeling/rules/MatchResourceOwner.spec.ts +7 -6
  70. package/tests/unit/modeling/rules/MatchUserProperty.spec.ts +6 -5
  71. package/tests/unit/modeling/rules/MatchUserRole.spec.ts +6 -5
  72. package/tests/unit/modeling/rules/restoring_rules.spec.ts +19 -21
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.19.22",
4
+ "version": "0.19.23",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -22,6 +22,7 @@ import { ExposedEntity } from './ExposedEntity.js'
22
22
  import { AccessRule, AccessRuleSchema } from './rules/AccessRule.js'
23
23
  import { RateLimitingConfiguration, RateLimitingConfigurationSchema } from './rules/RateLimitingConfiguration.js'
24
24
  import { restoreAccessRule } from './rules/index.js'
25
+ import { Exception } from '../exceptions/exception.js'
25
26
 
26
27
  /**
27
28
  * Contact information for the exposed API.
@@ -180,7 +181,7 @@ export class ApiModel extends DependentModel {
180
181
  *
181
182
  * The `key` is the key of the exposed entity. Using a Map to allow for quick lookups.
182
183
  */
183
- @observed({ deep: true }) accessor exposes: Map<string, ExposedEntity>
184
+ @observed() accessor exposes: Map<string, ExposedEntity>
184
185
  /**
185
186
  * Optional array of access rules that define the access control policies
186
187
  * for the API. These rules are used to enforce security and permissions
@@ -189,7 +190,7 @@ export class ApiModel extends DependentModel {
189
190
  * These rules apply to all exposed entities and actions. An API action
190
191
  * can declare its own access rules, which will override these.
191
192
  */
192
- @observed({ deep: true }) accessor accessRule: AccessRule[]
193
+ @observed() accessor accessRule: AccessRule[]
193
194
  /**
194
195
  * Optional configuration for API-wide rate limiting and throttling.
195
196
  * Defines rules to protect the API from overuse.
@@ -294,7 +295,10 @@ export class ApiModel extends DependentModel {
294
295
  } else if (typeof domain === 'object' && domain.kind === DataDomainKind) {
295
296
  instances.push(new DataDomain(domain))
296
297
  } else if (domain) {
297
- throw new Error(`Invalid domain provided. Expected a DataDomain instance or schema.`)
298
+ throw new Exception(`Invalid domain provided. Expected a DataDomain instance or schema.`, {
299
+ code: 'E_DOMAIN_INVALID',
300
+ help: 'It looks like the provided data domain is not a valid instance or domain schema.',
301
+ })
298
302
  }
299
303
  // Note that since we're using the `DependentModel` class, but the API Model can have only one dependency,
300
304
  // we keep the reference to the data domain under the `dependencyList` property.
@@ -302,7 +306,11 @@ export class ApiModel extends DependentModel {
302
306
  // process when the API model is loaded from the API.
303
307
  if (domain) {
304
308
  if (!domain.info.version) {
305
- throw new Error(`Domain ${domain.key} must have a version.`)
309
+ throw new Exception(`Domain ${domain.key} must have a version.`, {
310
+ code: 'E_DOMAIN_NO_VERSION',
311
+ // eslint-disable-next-line max-len
312
+ help: 'Before attaching the data domain, give it a version name. Only versioned domains can be attached to an API model.',
313
+ })
306
314
  }
307
315
  init.dependencyList = [{ key: domain.key, version: domain.info.version }]
308
316
  }
@@ -327,7 +335,7 @@ export class ApiModel extends DependentModel {
327
335
  this.exposes = new Map()
328
336
  }
329
337
  if (Array.isArray(init.accessRule)) {
330
- this.accessRule = init.accessRule.map((rule) => restoreAccessRule(rule))
338
+ this.accessRule = init.accessRule.map((rule) => restoreAccessRule(this, rule))
331
339
  } else {
332
340
  this.accessRule = []
333
341
  }
@@ -422,7 +430,10 @@ export class ApiModel extends DependentModel {
422
430
  exposeEntity(entity: AssociationTarget, options?: ExposeOptions): ExposedEntity {
423
431
  const domain = this.domain
424
432
  if (!domain) {
425
- throw new Error(`No domain attached to API model`)
433
+ throw new Exception(`No domain attached to API model`, {
434
+ code: 'E_NO_DOMAIN',
435
+ help: 'Attach a data domain to the API model before exposing entities.',
436
+ })
426
437
  }
427
438
  // checks whether the entity is already exposed as a root exposure.
428
439
  let existing: ExposedEntity | undefined
@@ -433,13 +444,17 @@ export class ApiModel extends DependentModel {
433
444
  }
434
445
  }
435
446
  if (existing) {
436
- // quietly return the existing exposure
437
- // TBD: should we throw an error here?
438
- return existing
447
+ throw new Exception(`Entity ${entity.key} is already exposed.`, {
448
+ code: 'E_ENTITY_ALREADY_EXPOSED',
449
+ help: 'Do not try to expose already exposed entity.',
450
+ })
439
451
  }
440
452
  const domainEntity = domain.findEntity(entity.key, entity.domain)
441
453
  if (!domainEntity) {
442
- throw new Error(`Entity not found in domain: ${entity.key}`)
454
+ throw new Exception(`Entity not found in domain: ${entity.key}`, {
455
+ code: 'E_NO_ENTITY',
456
+ help: 'Make sure the entity you are trying to expose exists in the data domain.',
457
+ })
443
458
  }
444
459
  const name = domainEntity.info.name || ''
445
460
  const segment = pluralize(name.toLocaleLowerCase())
@@ -491,7 +506,9 @@ export class ApiModel extends DependentModel {
491
506
  private followEntityAssociations(parentExposure: ExposedEntitySchema, options: ExposeOptions): void {
492
507
  const domain = this.domain
493
508
  if (!domain) {
494
- return
509
+ throw new Exception(`No domain attached to API model`, {
510
+ code: 'E_NO_DOMAIN',
511
+ })
495
512
  }
496
513
  const maxDepth = options.maxDepth ?? 6
497
514
  const follow = (currentEntity: AssociationTarget, parentKey: string, depth: number, currentPath: string[]) => {
@@ -609,7 +626,10 @@ export class ApiModel extends DependentModel {
609
626
  */
610
627
  removeExposedEntity(key: string): void {
611
628
  if (!this.exposes.has(key)) {
612
- throw new Error(`Exposed entity with key "${key}" not found.`)
629
+ throw new Exception(`Exposed entity with key "${key}" not found.`, {
630
+ code: 'E_NO_EXPOSURE',
631
+ help: 'The exposed entity you are trying to remove does not exist.',
632
+ })
613
633
  }
614
634
  this.removeWithChildren(key)
615
635
  }
@@ -667,7 +687,11 @@ export class ApiModel extends DependentModel {
667
687
  */
668
688
  attachDataDomain(domain: DataDomain): void {
669
689
  if (!domain.info.version) {
670
- throw new Error(`Cannot attach DataDomain without a version. Please set the version in the domain info.`)
690
+ throw new Exception(`Cannot attach DataDomain without a version. Please set the version in the domain info.`, {
691
+ code: 'E_DOMAIN_NO_VERSION',
692
+ // eslint-disable-next-line max-len
693
+ help: 'Before attaching the data domain, give it a version name. Only versioned domains can be attached to an API model.',
694
+ })
671
695
  }
672
696
  this.cleanForDomainChange()
673
697
  this.dependencies.set(domain.key, domain)
@@ -1,4 +1,5 @@
1
1
  import { observed, toRaw } from '../decorators/observed.js'
2
+ import { Exception } from '../exceptions/exception.js'
2
3
  import { ExposedEntityKind } from '../models/kinds.js'
3
4
  import { nanoid } from '../nanoid.js'
4
5
  import { Action } from './actions/Action.js'
@@ -94,12 +95,12 @@ export class ExposedEntity extends EventTarget {
94
95
  /**
95
96
  * The list of enabled API actions for this exposure (List/Read/Create/etc.)
96
97
  */
97
- @observed({ deep: true }) accessor actions: Action[]
98
+ @observed() accessor actions: Action[]
98
99
 
99
100
  /**
100
101
  * Optional array of access rules that define the access control policies for this exposure.
101
102
  */
102
- @observed({ deep: true }) accessor accessRule: AccessRule[] | undefined
103
+ @observed() accessor accessRule: AccessRule[] | undefined
103
104
 
104
105
  /**
105
106
  * Optional configuration for rate limiting for this exposure.
@@ -203,7 +204,7 @@ export class ExposedEntity extends EventTarget {
203
204
  this.parent = init.parent
204
205
  this.exposeOptions = init.exposeOptions
205
206
  this.actions = init.actions ? init.actions.map((a) => restoreAction(this, a)) : []
206
- this.accessRule = init.accessRule ? init.accessRule.map((ar) => restoreAccessRule(ar)) : []
207
+ this.accessRule = init.accessRule ? init.accessRule.map((ar) => restoreAccessRule(this, ar)) : []
207
208
  if (init.rateLimiting) {
208
209
  this.rateLimiting = new RateLimitingConfiguration(init.rateLimiting)
209
210
  }
@@ -223,6 +224,7 @@ export class ExposedEntity extends EventTarget {
223
224
  this.#notifying = false
224
225
  const event = new Event('change')
225
226
  this.dispatchEvent(event)
227
+ this.api.notifyChange()
226
228
  })
227
229
  }
228
230
 
@@ -273,13 +275,19 @@ export class ExposedEntity extends EventTarget {
273
275
  */
274
276
  setCollectionPath(path: string) {
275
277
  if (!this.hasCollection) {
276
- throw new Error(`Cannot set collection path on an exposure that does not have a collection`)
278
+ throw new Exception(`Cannot set collection path on an exposure that does not have a collection`, {
279
+ code: 'E_PATH_MISSING',
280
+ help: 'The exposed entity you are trying to set a collection path for does not have a collection.',
281
+ })
277
282
  }
278
283
  const cleaned = ensureLeadingSlash(path)
279
284
  // Ensure exactly one non-empty segment
280
285
  const segments = cleaned.split('/').filter(Boolean)
281
286
  if (segments.length !== 1) {
282
- throw new Error(`Collection path must contain exactly one segment. Received: "${path}"`)
287
+ throw new Exception(`Collection path must contain exactly one segment. Received: "${path}"`, {
288
+ code: 'E_PATH_SEGMENT_SIZE',
289
+ help: 'The set path must have a single path segment (e.g., `/products`)',
290
+ })
283
291
  }
284
292
  const normalizedCollection = `/${segments[0]}`
285
293
 
@@ -287,7 +295,10 @@ export class ExposedEntity extends EventTarget {
287
295
  if (this.isRoot) {
288
296
  const collision = this.api.findCollectionPathCollision(normalizedCollection, this.key)
289
297
  if (collision) {
290
- throw new Error(`Collection path "${normalizedCollection}" is already in use by another root entity.`)
298
+ throw new Exception(`Collection path "${normalizedCollection}" is already in use by another root entity.`, {
299
+ code: 'E_PATH_INUSE',
300
+ help: 'The set path is already in use by another root entity.',
301
+ })
291
302
  }
292
303
  }
293
304
 
@@ -321,22 +332,44 @@ export class ExposedEntity extends EventTarget {
321
332
 
322
333
  if (this.hasCollection) {
323
334
  if (!this.collectionPath) {
324
- throw new Error('Cannot set resource path: missing collection path for this exposure')
335
+ // Why do we throw this error? We should just create it from the resource path...
336
+ throw new Exception('Cannot set resource path: missing collection path for this exposure', {
337
+ code: 'E_PATH_MISSING',
338
+ help: 'Set the collection path on the exposed entity first.',
339
+ })
325
340
  }
326
341
  const colSegments = this.collectionPath.split('/').filter(Boolean)
327
342
  if (colSegments.length !== 1) {
328
- throw new Error(`Invalid stored collection path "${this.collectionPath}"`)
343
+ throw new Exception(`Invalid stored collection path "${this.collectionPath}"`, {
344
+ code: 'E_PATH_SEGMENT_SIZE',
345
+ help: 'The set collection path must have a single path segment (e.g., `/products`)',
346
+ })
329
347
  }
330
348
  if (segments.length !== 2) {
331
- throw new Error(`Resource path must be exactly two segments (collection + parameter). Received: "${cleaned}"`)
349
+ throw new Exception(
350
+ `Resource path must be exactly two segments (collection + parameter). Received: "${cleaned}"`,
351
+ {
352
+ code: 'E_PATH_SEGMENT_SIZE',
353
+ help: 'The set resource path must have exactly two segments (collection + parameter).',
354
+ }
355
+ )
332
356
  }
333
357
  const [s1, s2] = segments
334
358
  if (s1 !== colSegments[0]) {
335
- throw new Error(`Resource path must start with the collection segment "${colSegments[0]}". Received: "${s1}"`)
359
+ throw new Exception(
360
+ `Resource path must start with the collection segment "${colSegments[0]}". Received: "${s1}"`,
361
+ {
362
+ code: 'E_PATH_MISMATCH',
363
+ help: 'Set the resource path to the same value as the collection path + the parameter value.',
364
+ }
365
+ )
336
366
  }
337
367
  // s2 must be a parameter segment {name}
338
368
  if (!/^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(s2)) {
339
- throw new Error(`The second segment must be a parameter in braces, e.g. {id}. Received: "${s2}"`)
369
+ throw new Exception(`The second segment must be a parameter in braces, e.g. {id}. Received: "${s2}"`, {
370
+ code: 'E_PARAMETER_INVALID',
371
+ help: 'Use the braces to surround the parameter name, e.g., {productId}.',
372
+ })
340
373
  }
341
374
  if (this.resourcePath !== cleaned) {
342
375
  this.resourcePath = `/${s1}/${s2}`
@@ -346,15 +379,22 @@ export class ExposedEntity extends EventTarget {
346
379
 
347
380
  // No collection: allow exactly one segment
348
381
  if (segments.length !== 1) {
349
- throw new Error(
350
- `Resource path must contain exactly one segment when no collection is present. Received: "${cleaned}"`
382
+ throw new Exception(
383
+ `Resource path must contain exactly one segment when no collection is present. Received: "${cleaned}"`,
384
+ {
385
+ code: 'E_PATH_SEGMENT_SIZE',
386
+ help: 'The set resource path must have exactly one segment.',
387
+ }
351
388
  )
352
389
  }
353
390
  // Check for collision if this is a root entity (singleton case)
354
391
  if (this.isRoot) {
355
392
  const collision = this.api.findResourcePathCollision(cleaned, this.key)
356
393
  if (collision) {
357
- throw new Error(`Resource path "${cleaned}" is already in use by another root entity.`)
394
+ throw new Exception(`Resource path "${cleaned}" is already in use by another root entity.`, {
395
+ code: 'E_PATH_INUSE',
396
+ help: 'The set path is already in use by another root entity.',
397
+ })
358
398
  }
359
399
  }
360
400
 
@@ -468,7 +508,10 @@ export class ExposedEntity extends EventTarget {
468
508
  */
469
509
  addAction(schema: ApiActionSchema): Action {
470
510
  if (this.actions.some((action) => action.kind === schema.kind)) {
471
- throw new Error(`Action of kind "${schema.kind}" already exists for this exposure`)
511
+ throw new Exception(`Action of kind "${schema.kind}" already exists for this exposure`, {
512
+ code: 'E_ACTION_USED',
513
+ help: "There's no need to add an API action again.",
514
+ })
472
515
  }
473
516
  const action = restoreAction(this, schema)
474
517
  this.actions.push(action)
@@ -486,7 +529,10 @@ export class ExposedEntity extends EventTarget {
486
529
  createPaginationContract(): void {
487
530
  const entity = this.api.domain?.findEntity(this.entity.key, this.entity.domain)
488
531
  if (!entity) {
489
- throw new Error(`Entity "${this.entity.key}" not found"`)
532
+ throw new Exception(`Entity "${this.entity.key}" not found"`, {
533
+ code: 'E_ENTITY_NOT_FOUND',
534
+ help: 'The set entity does not exist.',
535
+ })
490
536
  }
491
537
  if (!this.paginationContract) {
492
538
  this.paginationContract = {
@@ -46,13 +46,13 @@ export class Action extends EventTarget implements ActionSchema {
46
46
  super()
47
47
  this.parent = parent
48
48
  this.kind = state.kind || ''
49
- this.accessRule = state.accessRule ? state.accessRule.map((rule) => restoreAccessRule(rule)) : []
49
+ this.accessRule = state.accessRule ? state.accessRule.map((rule) => restoreAccessRule(this, rule)) : []
50
50
  this.rateLimiting = state.rateLimiting ? new RateLimitingConfiguration(state.rateLimiting) : undefined
51
51
  }
52
52
 
53
53
  notifyChange() {
54
54
  this.dispatchEvent(new Event('change'))
55
- // this.parent.api.notifyChange()
55
+ this.parent.notifyChange()
56
56
  }
57
57
 
58
58
  toJSON(): ActionSchema {
@@ -1,3 +1,7 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
4
+
1
5
  export interface AccessRuleSchema {
2
6
  /**
3
7
  * The unique identifier for the access rule.
@@ -11,9 +15,11 @@ export interface AccessRuleSchema {
11
15
  */
12
16
  export class AccessRule extends EventTarget implements AccessRuleSchema {
13
17
  readonly type: string
18
+ readonly parent: ExposedEntity | ApiModel | Action
14
19
 
15
- constructor(state: Partial<AccessRuleSchema> = {}) {
20
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AccessRuleSchema> = {}) {
16
21
  super()
22
+ this.parent = parent
17
23
  this.type = state.type ?? ''
18
24
  }
19
25
 
@@ -25,5 +31,6 @@ export class AccessRule extends EventTarget implements AccessRuleSchema {
25
31
 
26
32
  notifyChange() {
27
33
  this.dispatchEvent(new Event('change'))
34
+ this.parent.notifyChange()
28
35
  }
29
36
  }
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
 
3
6
  /**
@@ -17,8 +20,8 @@ export interface AllowAuthenticatedAccessRuleSchema extends AccessRuleSchema {
17
20
  export class AllowAuthenticatedAccessRule extends AccessRule implements AllowAuthenticatedAccessRuleSchema {
18
21
  override readonly type: 'allowAuthenticated'
19
22
 
20
- constructor(state: Partial<AllowAuthenticatedAccessRuleSchema> = {}) {
21
- super(state)
23
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowAuthenticatedAccessRuleSchema> = {}) {
24
+ super(parent, state)
22
25
  this.type = 'allowAuthenticated'
23
26
  }
24
27
  }
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
 
3
6
  /**
@@ -17,8 +20,8 @@ export interface AllowPublicAccessRuleSchema extends AccessRuleSchema {
17
20
  export class AllowPublicAccessRule extends AccessRule implements AllowPublicAccessRuleSchema {
18
21
  override readonly type: 'allowPublic'
19
22
 
20
- constructor(state: Partial<AllowPublicAccessRuleSchema> = {}) {
21
- super(state)
23
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowPublicAccessRuleSchema> = {}) {
24
+ super(parent, state)
22
25
  this.type = 'allowPublic'
23
26
  }
24
27
  }
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
  import { observed, toRaw } from '../../decorators/observed.js'
3
6
 
@@ -24,8 +27,8 @@ export class MatchEmailDomainAccessRule extends AccessRule implements MatchEmail
24
27
 
25
28
  @observed({ deep: true }) accessor domains: string[]
26
29
 
27
- constructor(state: Partial<MatchEmailDomainAccessRuleSchema> = {}) {
28
- super(state)
30
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchEmailDomainAccessRuleSchema> = {}) {
31
+ super(parent, state)
29
32
  this.type = 'matchEmailDomain'
30
33
  this.domains = state.domains ? [...state.domains] : []
31
34
  }
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
  import { observed } from '../../decorators/observed.js'
3
6
 
@@ -38,8 +41,8 @@ export class MatchResourceOwnerAccessRule extends AccessRule implements MatchRes
38
41
  @observed() accessor property: string | undefined
39
42
  @observed() accessor target: 'property' | 'user-entity'
40
43
 
41
- constructor(state: Partial<MatchResourceOwnerAccessRuleSchema> = {}) {
42
- super(state)
44
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchResourceOwnerAccessRuleSchema> = {}) {
45
+ super(parent, state)
43
46
  this.type = 'matchResourceOwner'
44
47
  this.property = state.property
45
48
  this.target = state.target ?? 'property'
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
  import { observed } from '../../decorators/observed.js'
3
6
 
@@ -27,8 +30,8 @@ export class MatchUserPropertyAccessRule extends AccessRule implements MatchUser
27
30
  @observed() accessor property: string
28
31
  @observed() accessor value: string
29
32
 
30
- constructor(state: Partial<MatchUserPropertyAccessRuleSchema> = {}) {
31
- super(state)
33
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserPropertyAccessRuleSchema> = {}) {
34
+ super(parent, state)
32
35
  this.type = 'matchUserProperty'
33
36
  this.property = state.property ?? ''
34
37
  this.value = state.value ?? ''
@@ -1,3 +1,6 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
1
4
  import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
2
5
  import { observed, toRaw } from '../../decorators/observed.js'
3
6
 
@@ -28,8 +31,8 @@ export class MatchUserRoleAccessRule extends AccessRule implements MatchUserRole
28
31
 
29
32
  @observed({ deep: true }) accessor role: string[]
30
33
 
31
- constructor(state: Partial<MatchUserRoleAccessRuleSchema> = {}) {
32
- super(state)
34
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserRoleAccessRuleSchema> = {}) {
35
+ super(parent, state)
33
36
  this.type = 'matchUserRole'
34
37
  this.role = state.role ? [...state.role] : []
35
38
  }
@@ -2,10 +2,11 @@ import { test } from '@japa/runner'
2
2
  import { Action, AccessRule, RateLimitingConfiguration } from '../../../../src/modeling/index.js'
3
3
  import { type ActionSchema } from '../../../../src/modeling/actions/Action.js'
4
4
  import { type ExposedEntity } from '../../../../src/modeling/ExposedEntity.js'
5
+ import { mockExposedEntity } from './helpers.js'
5
6
 
6
7
  test.group('Action', () => {
7
8
  test('initializes with default values', ({ assert }) => {
8
- const action = new Action({} as unknown as ExposedEntity)
9
+ const action = new Action(mockExposedEntity())
9
10
  assert.equal(action.kind, '')
10
11
  assert.deepEqual(action.accessRule, [])
11
12
  assert.isUndefined(action.rateLimiting)
@@ -17,7 +18,7 @@ test.group('Action', () => {
17
18
  accessRule: [{ type: 'allowPublic' }],
18
19
  rateLimiting: { rules: [] },
19
20
  }
20
- const action = new Action({} as unknown as ExposedEntity, schema)
21
+ const action = new Action(mockExposedEntity(), schema)
21
22
  assert.equal(action.kind, 'read')
22
23
  assert.lengthOf(action.accessRule, 1)
23
24
  assert.instanceOf(action.accessRule[0], AccessRule)
@@ -26,7 +27,7 @@ test.group('Action', () => {
26
27
  }).tags(['@modeling', '@action'])
27
28
 
28
29
  test('serializes to JSON', ({ assert }) => {
29
- const action = new Action({} as unknown as ExposedEntity, {
30
+ const action = new Action(mockExposedEntity(), {
30
31
  kind: 'write',
31
32
  accessRule: [{ type: 'allowPublic' }],
32
33
  rateLimiting: { rules: [] },
@@ -39,7 +40,7 @@ test.group('Action', () => {
39
40
  }).tags(['@modeling', '@action'])
40
41
 
41
42
  test('notifies change when kind changes', async ({ assert }) => {
42
- const action = new Action({} as unknown as ExposedEntity, { kind: 'read' })
43
+ const action = new Action(mockExposedEntity(), { kind: 'read' })
43
44
  let notified = false
44
45
  action.addEventListener('change', () => {
45
46
  notified = true
@@ -53,19 +54,19 @@ test.group('Action', () => {
53
54
  }).tags(['@modeling', '@action', '@observed'])
54
55
 
55
56
  test('notifies change when accessRule array is replaced', async ({ assert }) => {
56
- const action = new Action({} as unknown as ExposedEntity)
57
+ const action = new Action(mockExposedEntity())
57
58
  let notified = false
58
59
  action.addEventListener('change', () => {
59
60
  notified = true
60
61
  })
61
62
 
62
- action.accessRule = [new AccessRule({ type: 'allowPublic' })]
63
+ action.accessRule = [new AccessRule(mockExposedEntity(), { type: 'allowPublic' })]
63
64
  await Promise.resolve()
64
65
  assert.isTrue(notified)
65
66
  }).tags(['@modeling', '@action', '@observed'])
66
67
 
67
68
  test('notifies change when rateLimiting is replaced', async ({ assert }) => {
68
- const action = new Action({} as unknown as ExposedEntity)
69
+ const action = new Action(mockExposedEntity())
69
70
  let notified = false
70
71
  action.addEventListener('change', () => {
71
72
  notified = true
@@ -78,7 +79,7 @@ test.group('Action', () => {
78
79
 
79
80
  test('constructor copies accessRule array (immutability)', ({ assert }) => {
80
81
  const rules = [{ type: 'allowPublic' }]
81
- const action = new Action({} as unknown as ExposedEntity, { kind: 'read', accessRule: rules })
82
+ const action = new Action(mockExposedEntity(), { kind: 'read', accessRule: rules })
82
83
 
83
84
  // Modify original array
84
85
  rules.push({ type: 'other' })
@@ -89,7 +90,7 @@ test.group('Action', () => {
89
90
  }).tags(['@modeling', '@action', '@immutability'])
90
91
 
91
92
  test('toJSON returns safe copy', ({ assert }) => {
92
- const action = new Action({} as unknown as ExposedEntity, {
93
+ const action = new Action(mockExposedEntity(), {
93
94
  kind: 'read',
94
95
  accessRule: [{ type: 'allowPublic' }],
95
96
  rateLimiting: { rules: [] },
@@ -110,7 +111,8 @@ test.group('Action', () => {
110
111
 
111
112
  test('getAllRules() aggregates rules from action and parent', ({ assert }) => {
112
113
  const parent = {
113
- getAllRules: () => [new AccessRule({ type: 'allowPublic' })],
114
+ getAllRules: () => [new AccessRule(mockExposedEntity(), { type: 'allowPublic' })],
115
+ notifyChange: () => {},
114
116
  } as unknown as ExposedEntity
115
117
 
116
118
  const action = new Action(parent, {
@@ -127,6 +129,7 @@ test.group('Action', () => {
127
129
  const parentLimiter = new RateLimitingConfiguration({ rules: [{ rate: 10, interval: 'second' }] })
128
130
  const parent = {
129
131
  getAllRateLimiters: () => [parentLimiter],
132
+ notifyChange: () => {},
130
133
  } as unknown as ExposedEntity
131
134
 
132
135
  const action = new Action(parent, {
@@ -1,16 +1,17 @@
1
1
  import { test } from '@japa/runner'
2
2
  import { CreateAction } from '../../../../src/modeling/actions/CreateAction.js'
3
3
  import { AccessRule } from '../../../../src/modeling/rules/index.js'
4
+ import { mockExposedEntity } from './helpers.js'
4
5
 
5
6
  test.group('CreateAction', () => {
6
7
  test('initializes with default values', ({ assert }) => {
7
- const action = new CreateAction({} as any)
8
+ const action = new CreateAction(mockExposedEntity())
8
9
  assert.equal(action.kind, 'create')
9
10
  assert.isEmpty(action.accessRule) // Inherited from Action
10
11
  }).tags(['@modeling', '@action', '@create-action'])
11
12
 
12
13
  test('initializes with inherited values', ({ assert }) => {
13
- const action = new CreateAction({} as any, {
14
+ const action = new CreateAction(mockExposedEntity(), {
14
15
  accessRule: [{ type: 'allowPublic' }],
15
16
  })
16
17
 
@@ -22,7 +23,7 @@ test.group('CreateAction', () => {
22
23
  test('constructor copies arrays (immutability)', ({ assert }) => {
23
24
  const rules = [{ type: 'allowPublic' }]
24
25
 
25
- const action = new CreateAction({} as any, {
26
+ const action = new CreateAction(mockExposedEntity(), {
26
27
  accessRule: rules,
27
28
  })
28
29
 
@@ -35,7 +36,7 @@ test.group('CreateAction', () => {
35
36
  }).tags(['@modeling', '@action', '@create-action', '@immutability'])
36
37
 
37
38
  test('toJSON returns valid schema', ({ assert }) => {
38
- const action = new CreateAction({} as any, {
39
+ const action = new CreateAction(mockExposedEntity(), {
39
40
  accessRule: [{ type: 'allowPublic' }],
40
41
  })
41
42
 
@@ -51,14 +52,14 @@ test.group('CreateAction', () => {
51
52
  }).tags(['@modeling', '@action', '@create-action', '@serialization'])
52
53
 
53
54
  test('notifies change when inherited property changes', async ({ assert }) => {
54
- const action = new CreateAction({} as any)
55
+ const action = new CreateAction(mockExposedEntity())
55
56
  let notified = false
56
57
  action.addEventListener('change', () => {
57
58
  notified = true
58
59
  })
59
60
 
60
61
  // Modify inherited property
61
- action.accessRule = [new AccessRule({ type: 'allowPublic' })]
62
+ action.accessRule = [new AccessRule(mockExposedEntity(), { type: 'allowPublic' })]
62
63
  await Promise.resolve()
63
64
  assert.isTrue(notified)
64
65
  }).tags(['@modeling', '@action', '@create-action', '@observed'])