@api-client/core 0.19.41 → 0.19.42

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 (64) hide show
  1. package/build/src/index.d.ts +1 -1
  2. package/build/src/index.d.ts.map +1 -1
  3. package/build/src/index.js.map +1 -1
  4. package/build/src/modeling/RuntimeApiModel.d.ts +20 -0
  5. package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
  6. package/build/src/modeling/RuntimeApiModel.js +133 -0
  7. package/build/src/modeling/RuntimeApiModel.js.map +1 -1
  8. package/build/src/modeling/index.d.ts +1 -0
  9. package/build/src/modeling/index.d.ts.map +1 -1
  10. package/build/src/modeling/index.js.map +1 -1
  11. package/build/src/modeling/rules/AccessRule.d.ts +40 -1
  12. package/build/src/modeling/rules/AccessRule.d.ts.map +1 -1
  13. package/build/src/modeling/rules/AccessRule.js +44 -2
  14. package/build/src/modeling/rules/AccessRule.js.map +1 -1
  15. package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -1
  16. package/build/src/modeling/rules/AllowAuthenticated.js +9 -2
  17. package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -1
  18. package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -1
  19. package/build/src/modeling/rules/AllowPublic.js +9 -2
  20. package/build/src/modeling/rules/AllowPublic.js.map +1 -1
  21. package/build/src/modeling/rules/LifecycleStatus.d.ts +36 -0
  22. package/build/src/modeling/rules/LifecycleStatus.d.ts.map +1 -0
  23. package/build/src/modeling/rules/LifecycleStatus.js +60 -0
  24. package/build/src/modeling/rules/LifecycleStatus.js.map +1 -0
  25. package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -1
  26. package/build/src/modeling/rules/MatchEmailDomain.js +9 -2
  27. package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -1
  28. package/build/src/modeling/rules/MatchResourceAttribute.d.ts +38 -0
  29. package/build/src/modeling/rules/MatchResourceAttribute.d.ts.map +1 -0
  30. package/build/src/modeling/rules/MatchResourceAttribute.js +68 -0
  31. package/build/src/modeling/rules/MatchResourceAttribute.js.map +1 -0
  32. package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -1
  33. package/build/src/modeling/rules/MatchResourceOwner.js +8 -2
  34. package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -1
  35. package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -1
  36. package/build/src/modeling/rules/MatchUserProperty.js +9 -2
  37. package/build/src/modeling/rules/MatchUserProperty.js.map +1 -1
  38. package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -1
  39. package/build/src/modeling/rules/MatchUserRole.js +9 -2
  40. package/build/src/modeling/rules/MatchUserRole.js.map +1 -1
  41. package/build/src/modeling/rules/index.d.ts +8 -6
  42. package/build/src/modeling/rules/index.d.ts.map +1 -1
  43. package/build/src/modeling/rules/index.js +8 -2
  44. package/build/src/modeling/rules/index.js.map +1 -1
  45. package/build/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/modeling/RuntimeApiModel.ts +166 -2
  48. package/src/modeling/rules/AccessRule.ts +70 -2
  49. package/src/modeling/rules/AllowAuthenticated.ts +13 -2
  50. package/src/modeling/rules/AllowPublic.ts +13 -2
  51. package/src/modeling/rules/LifecycleStatus.ts +71 -0
  52. package/src/modeling/rules/MatchEmailDomain.ts +13 -2
  53. package/src/modeling/rules/MatchResourceAttribute.ts +82 -0
  54. package/src/modeling/rules/MatchResourceOwner.ts +12 -2
  55. package/src/modeling/rules/MatchUserProperty.ts +13 -2
  56. package/src/modeling/rules/MatchUserRole.ts +13 -2
  57. package/tests/unit/modeling/RuntimeApiModel.spec.ts +189 -1
  58. package/tests/unit/modeling/actions/Action.spec.ts +2 -2
  59. package/tests/unit/modeling/actions/CreateAction.spec.ts +1 -1
  60. package/tests/unit/modeling/actions/ReadAction.spec.ts +2 -2
  61. package/tests/unit/modeling/exposed_entity.spec.ts +1 -1
  62. package/tests/unit/modeling/rules/AccessRule.spec.ts +5 -5
  63. package/tests/unit/modeling/rules/LifecycleStatus.spec.ts +55 -0
  64. package/tests/unit/modeling/rules/MatchResourceAttribute.spec.ts +66 -0
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.41",
4
+ "version": "0.19.42",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -7,6 +7,7 @@ import type { Action } from './actions/Action.js'
7
7
  import { SemanticType } from './Semantics.js'
8
8
  import type { DomainEntity } from './DomainEntity.js'
9
9
  import type { DomainProperty } from './DomainProperty.js'
10
+ import { AccessRule, AccessRuleExecutionPhase } from './rules/AccessRule.js'
10
11
 
11
12
  /**
12
13
  * Identifies a specific exposed entity and its action kind.
@@ -42,6 +43,19 @@ export interface RuntimeResolvedAction {
42
43
  params: Record<string, string>
43
44
  }
44
45
 
46
+ export type RuleEvaluator = (rule: AccessRule) => Promise<boolean | undefined> | boolean | undefined
47
+
48
+ interface PhaseRules {
49
+ permissionRules: AccessRule[]
50
+ mandatoryRules: AccessRule[]
51
+ }
52
+
53
+ interface ActionRulesCache {
54
+ preFetch: PhaseRules
55
+ fetch: PhaseRules
56
+ postFetch: PhaseRules
57
+ }
58
+
45
59
  /**
46
60
  * An optimized API Model subclass designed for fast runtime lookups.
47
61
  * It pre-compiles the RoutingMap into a radix tree for O(log N) or faster endpoint resolution.
@@ -74,6 +88,11 @@ export class RuntimeApiModel extends ApiModel {
74
88
  role?: DomainProperty
75
89
  } = {}
76
90
 
91
+ /**
92
+ * Cached access rules for each action to avoid runtime computation overhead.
93
+ */
94
+ #actionRulesCache = new WeakMap<Action, ActionRulesCache>()
95
+
77
96
  constructor(schema: RuntimeApiModelSchema, domainSchema: DataDomainSchema) {
78
97
  super(schema, domainSchema)
79
98
 
@@ -82,9 +101,93 @@ export class RuntimeApiModel extends ApiModel {
82
101
  }
83
102
 
84
103
  this.#cacheEntitiesAndProperties()
104
+ this.#precomputeAccessRules()
105
+ }
106
+
107
+ #precomputeAccessRules(): void {
108
+ for (const entity of this.exposes.values()) {
109
+ for (const action of entity.actions) {
110
+ this.#actionRulesCache.set(action, this.#computeEffectiveRules(action, entity))
111
+ }
112
+ }
113
+ }
114
+
115
+ #computeEffectiveRules(action: Action, entity: ExposedEntity): ActionRulesCache {
116
+ const hierarchy: AccessRule[][] = []
117
+
118
+ if (action.accessRule && action.accessRule.length > 0) {
119
+ hierarchy.push(action.accessRule)
120
+ }
121
+
122
+ let currentEntity: ExposedEntity | undefined = entity
123
+ while (currentEntity) {
124
+ if (currentEntity.accessRule && currentEntity.accessRule.length > 0) {
125
+ hierarchy.push(currentEntity.accessRule)
126
+ }
127
+ currentEntity = currentEntity.parent ? this.exposes.get(currentEntity.parent.key) : undefined
128
+ }
129
+
130
+ if (this.accessRule && this.accessRule.length > 0) {
131
+ hierarchy.push(this.accessRule)
132
+ }
133
+
134
+ const result: ActionRulesCache = {
135
+ preFetch: {
136
+ permissionRules: [],
137
+ mandatoryRules: [],
138
+ },
139
+ fetch: {
140
+ permissionRules: [],
141
+ mandatoryRules: [],
142
+ },
143
+ postFetch: {
144
+ permissionRules: [],
145
+ mandatoryRules: [],
146
+ },
147
+ }
148
+
149
+ const seenTypes = new Set<string>()
150
+
151
+ for (const rulesLevel of hierarchy) {
152
+ const typesInThisLevel = new Set<string>()
153
+ for (const rule of rulesLevel) {
154
+ const phase = rule.metadata[action.kind as keyof typeof rule.metadata]
155
+
156
+ // If the rule does not specify an execution phase for this action kind, it is not evaluated
157
+ if (!phase) {
158
+ continue
159
+ }
160
+
161
+ // Shadowing: If a rule of the same type was defined closer to the action, ignore this one.
162
+ if (seenTypes.has(rule.type)) {
163
+ continue
164
+ }
165
+ typesInThisLevel.add(rule.type)
166
+
167
+ let bucket
168
+ if (phase === AccessRuleExecutionPhase.POST_FETCH) {
169
+ bucket = result.postFetch
170
+ } else if (phase === AccessRuleExecutionPhase.FETCH) {
171
+ bucket = result.fetch
172
+ } else {
173
+ bucket = result.preFetch
174
+ }
175
+
176
+ if (rule.mandatory) {
177
+ bucket.mandatoryRules.push(rule)
178
+ } else {
179
+ bucket.permissionRules.push(rule)
180
+ }
181
+ }
182
+ for (const type of typesInThisLevel) {
183
+ seenTypes.add(type)
184
+ }
185
+ }
186
+
187
+ return result
85
188
  }
86
189
 
87
- #cacheEntitiesAndProperties() {
190
+ #cacheEntitiesAndProperties(): void {
88
191
  if (!this.user || !this.domain) {
89
192
  return
90
193
  }
@@ -109,7 +212,7 @@ export class RuntimeApiModel extends ApiModel {
109
212
  }
110
213
  }
111
214
 
112
- #initializeRouter(routingMap: RoutingMap) {
215
+ #initializeRouter(routingMap: RoutingMap): void {
113
216
  for (const [method, definitions] of Object.entries(routingMap)) {
114
217
  const parsedRoutes = []
115
218
  for (const def of definitions) {
@@ -167,6 +270,67 @@ export class RuntimeApiModel extends ApiModel {
167
270
  }
168
271
  }
169
272
 
273
+ /**
274
+ * Evaluates access rules for a given action and phase.
275
+ *
276
+ * The evaluation process follows two phases per execution phase (PRE_FETCH, FETCH, POST_FETCH):
277
+ * 1. Mandatory Phase: All rules marked as `mandatory: true` across all levels must return true.
278
+ * If any fail, the request is immediately rejected.
279
+ * 2. Permission Phase: Evaluation follows the hierarchy from most specific to most general:
280
+ * Action -> Endpoint (ExposedEntity) -> API Model.
281
+ * If an explicit allow (true) or deny (false) is hit, evaluation stops and returns the result.
282
+ * If no rules match (all return undefined), the request is rejected by default.
283
+ *
284
+ * @param action The resolved action to evaluate.
285
+ * @param evaluator A callback that evaluates a single rule. Should return true (allow),
286
+ * false (deny), or undefined (no hit).
287
+ * @param phase The execution phase to evaluate.
288
+ * @returns A promise that resolves to true if access is granted, false if denied.
289
+ */
290
+ async evaluateAccess(
291
+ action: RuntimeResolvedAction,
292
+ evaluator: RuleEvaluator,
293
+ phase: AccessRuleExecutionPhase
294
+ ): Promise<boolean> {
295
+ let cachedRules = this.#actionRulesCache.get(action.action)
296
+ if (!cachedRules) {
297
+ // Fallback if somehow action is not cached (e.g. dynamically added after initialization or in tests)
298
+ cachedRules = this.#computeEffectiveRules(action.action, action.entity)
299
+ this.#actionRulesCache.set(action.action, cachedRules)
300
+ }
301
+
302
+ let rulesForPhase
303
+ if (phase === AccessRuleExecutionPhase.POST_FETCH) {
304
+ rulesForPhase = cachedRules.postFetch
305
+ } else if (phase === AccessRuleExecutionPhase.FETCH) {
306
+ rulesForPhase = cachedRules.fetch
307
+ } else {
308
+ rulesForPhase = cachedRules.preFetch
309
+ }
310
+
311
+ // Step 1: Mandatory Phase
312
+ for (const rule of rulesForPhase.mandatoryRules) {
313
+ const result = await evaluator(rule)
314
+ if (result !== true) {
315
+ return false // Immediately reject if any mandatory rule fails
316
+ }
317
+ }
318
+
319
+ // Step 2-4: Permission Phase
320
+ for (const rule of rulesForPhase.permissionRules) {
321
+ const result = await evaluator(rule)
322
+ if (result === true) {
323
+ return true
324
+ }
325
+ if (result === false) {
326
+ return false
327
+ }
328
+ }
329
+
330
+ // Default: no hit
331
+ return false
332
+ }
333
+
170
334
  override toJSON(): RuntimeApiModelSchema {
171
335
  const base = super.toJSON() as RuntimeApiModelSchema
172
336
 
@@ -2,12 +2,47 @@ import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
4
 
5
+ export enum AccessRuleExecutionPhase {
6
+ /**
7
+ * The action that happens before the fetch.
8
+ * For example MatchUserRole and MatchUserProperty are executed in this phase.
9
+ */
10
+ PRE_FETCH = 'pre-fetch',
11
+ /**
12
+ * The action that happens during the fetch, for example injecting clauses into a query.
13
+ */
14
+ FETCH = 'fetch',
15
+ /**
16
+ * The action that happens after the resource is fetched from the database.
17
+ */
18
+ POST_FETCH = 'post-fetch',
19
+ }
20
+
5
21
  export interface AccessRuleSchema {
6
22
  /**
7
23
  * The unique identifier for the access rule.
8
24
  * This is used to reference the rule in the API configuration.
9
25
  */
10
26
  type: string
27
+
28
+ /**
29
+ * Whether this rule is mandatory. If true, the rule must be satisfied for the action to be allowed,
30
+ * regardless of other rules. If a mandatory rule is not satisfied, the action is denied.
31
+ */
32
+ mandatory?: boolean
33
+ }
34
+
35
+ /**
36
+ * The definition of when exactly the access rule should be evaluated.
37
+ * If the action is not specified, the rule is not evaluated for that action.
38
+ */
39
+ export interface AccessRuleExecutionMetadata {
40
+ readonly list?: AccessRuleExecutionPhase
41
+ readonly create?: AccessRuleExecutionPhase
42
+ readonly search?: AccessRuleExecutionPhase
43
+ readonly read?: AccessRuleExecutionPhase
44
+ readonly update?: AccessRuleExecutionPhase
45
+ readonly delete?: AccessRuleExecutionPhase
11
46
  }
12
47
 
13
48
  /**
@@ -17,16 +52,49 @@ export class AccessRule extends EventTarget implements AccessRuleSchema {
17
52
  readonly type: string
18
53
  readonly parent: ExposedEntity | ApiModel | Action
19
54
 
20
- constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AccessRuleSchema> = {}) {
55
+ /**
56
+ * Whether this rule is mandatory. If true, the rule must be satisfied for the action to be allowed,
57
+ * regardless of other rules. If a mandatory rule is not satisfied, the action is denied.
58
+ */
59
+ mandatory: boolean
60
+
61
+ /**
62
+ * The execution phase dictates when the rule is evaluated during the request lifecycle.
63
+ * - 'pre-fetch': Evaluated before the underlying resource is fetched from the database.
64
+ * - 'post-fetch': Evaluated after the underlying resource is fetched from the database.
65
+ *
66
+ * In some situations (like List and MatchResourceOwner) this is executed when a query is
67
+ * build to inject additional clauses for example.
68
+ */
69
+ #metadata: AccessRuleExecutionMetadata
70
+
71
+ get metadata(): AccessRuleExecutionMetadata {
72
+ if (!this.#metadata) {
73
+ throw new Error('Metadata not set for access rule ' + this.type)
74
+ }
75
+ return this.#metadata
76
+ }
77
+
78
+ constructor(
79
+ parent: ExposedEntity | ApiModel | Action,
80
+ metadata: AccessRuleExecutionMetadata,
81
+ state: Partial<AccessRuleSchema> = {}
82
+ ) {
21
83
  super()
22
84
  this.parent = parent
23
85
  this.type = state.type ?? ''
86
+ this.mandatory = state.mandatory ?? false
87
+ this.#metadata = metadata
24
88
  }
25
89
 
26
90
  toJSON(): AccessRuleSchema {
27
- return {
91
+ const result: AccessRuleSchema = {
28
92
  type: this.type,
29
93
  }
94
+ if (this.mandatory) {
95
+ result.mandatory = true
96
+ }
97
+ return result
30
98
  }
31
99
 
32
100
  notifyChange() {
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
 
6
6
  /**
7
7
  * The action is allowed for any authenticated user.
@@ -21,7 +21,18 @@ export class AllowAuthenticatedAccessRule extends AccessRule implements AllowAut
21
21
  override readonly type: 'allowAuthenticated'
22
22
 
23
23
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowAuthenticatedAccessRuleSchema> = {}) {
24
- super(parent, state)
24
+ super(
25
+ parent,
26
+ {
27
+ list: AccessRuleExecutionPhase.PRE_FETCH,
28
+ create: AccessRuleExecutionPhase.PRE_FETCH,
29
+ search: AccessRuleExecutionPhase.PRE_FETCH,
30
+ read: AccessRuleExecutionPhase.PRE_FETCH,
31
+ update: AccessRuleExecutionPhase.PRE_FETCH,
32
+ delete: AccessRuleExecutionPhase.PRE_FETCH,
33
+ },
34
+ state
35
+ )
25
36
  this.type = 'allowAuthenticated'
26
37
  }
27
38
  }
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
 
6
6
  /**
7
7
  * The action is allowed for all users, including unauthenticated ones.
@@ -21,7 +21,18 @@ export class AllowPublicAccessRule extends AccessRule implements AllowPublicAcce
21
21
  override readonly type: 'allowPublic'
22
22
 
23
23
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowPublicAccessRuleSchema> = {}) {
24
- super(parent, state)
24
+ super(
25
+ parent,
26
+ {
27
+ list: AccessRuleExecutionPhase.PRE_FETCH,
28
+ create: AccessRuleExecutionPhase.PRE_FETCH,
29
+ search: AccessRuleExecutionPhase.PRE_FETCH,
30
+ read: AccessRuleExecutionPhase.PRE_FETCH,
31
+ update: AccessRuleExecutionPhase.PRE_FETCH,
32
+ delete: AccessRuleExecutionPhase.PRE_FETCH,
33
+ },
34
+ state
35
+ )
25
36
  this.type = 'allowPublic'
26
37
  }
27
38
  }
@@ -0,0 +1,71 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
+ import { observed, toRaw } from '../../decorators/observed.js'
6
+
7
+ /**
8
+ * A specialized rule for entities using Status Semantics;
9
+ * ensures specific status records (e.g. "Archived" or "Draft") are granted or restricted.
10
+ *
11
+ * Since the data domain requires for an entity to have at the most one Status semantic, it is
12
+ * clear which field to use.
13
+ */
14
+ export interface LifecycleStatusAccessRuleSchema extends AccessRuleSchema {
15
+ type: 'lifecycleStatus'
16
+
17
+ /**
18
+ * The statuses that are allowed access. If the resource's status is not in this list,
19
+ * access will be denied by this rule (or un-handled if permission phase).
20
+ */
21
+ allowedStatuses?: string[]
22
+
23
+ /**
24
+ * The statuses that are explicitly denied access. If the resource's status is in this list,
25
+ * access will be denied.
26
+ */
27
+ deniedStatuses?: string[]
28
+ }
29
+
30
+ /**
31
+ * A specialized rule for entities using Status Semantics;
32
+ * ensures specific status records (e.g. "Archived" or "Draft") are granted or restricted.
33
+ */
34
+ export class LifecycleStatusAccessRule extends AccessRule implements LifecycleStatusAccessRuleSchema {
35
+ override readonly type: 'lifecycleStatus'
36
+
37
+ @observed({ deep: true }) accessor allowedStatuses: string[]
38
+ @observed({ deep: true }) accessor deniedStatuses: string[]
39
+
40
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<LifecycleStatusAccessRuleSchema> = {}) {
41
+ super(
42
+ parent,
43
+ {
44
+ list: AccessRuleExecutionPhase.FETCH,
45
+ search: AccessRuleExecutionPhase.FETCH,
46
+ read: AccessRuleExecutionPhase.POST_FETCH,
47
+ update: AccessRuleExecutionPhase.POST_FETCH,
48
+ delete: AccessRuleExecutionPhase.POST_FETCH,
49
+ },
50
+ state
51
+ )
52
+ this.type = 'lifecycleStatus'
53
+ this.allowedStatuses = state.allowedStatuses ? [...state.allowedStatuses] : []
54
+ this.deniedStatuses = state.deniedStatuses ? [...state.deniedStatuses] : []
55
+ }
56
+
57
+ override toJSON(): LifecycleStatusAccessRuleSchema {
58
+ const json: LifecycleStatusAccessRuleSchema = {
59
+ ...(super.toJSON() as LifecycleStatusAccessRuleSchema),
60
+ }
61
+
62
+ if (this.allowedStatuses.length > 0) {
63
+ json.allowedStatuses = structuredClone(toRaw(this, this.allowedStatuses)) as string[]
64
+ }
65
+ if (this.deniedStatuses.length > 0) {
66
+ json.deniedStatuses = structuredClone(toRaw(this, this.deniedStatuses)) as string[]
67
+ }
68
+
69
+ return json
70
+ }
71
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
  import { observed, toRaw } from '../../decorators/observed.js'
6
6
 
7
7
  /**
@@ -28,7 +28,18 @@ export class MatchEmailDomainAccessRule extends AccessRule implements MatchEmail
28
28
  @observed({ deep: true }) accessor domains: string[]
29
29
 
30
30
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchEmailDomainAccessRuleSchema> = {}) {
31
- super(parent, state)
31
+ super(
32
+ parent,
33
+ {
34
+ list: AccessRuleExecutionPhase.PRE_FETCH,
35
+ create: AccessRuleExecutionPhase.PRE_FETCH,
36
+ search: AccessRuleExecutionPhase.PRE_FETCH,
37
+ read: AccessRuleExecutionPhase.PRE_FETCH,
38
+ update: AccessRuleExecutionPhase.PRE_FETCH,
39
+ delete: AccessRuleExecutionPhase.PRE_FETCH,
40
+ },
41
+ state
42
+ )
32
43
  this.type = 'matchEmailDomain'
33
44
  this.domains = state.domains ? [...state.domains] : []
34
45
  }
@@ -0,0 +1,82 @@
1
+ import type { Action } from '../actions/Action.js'
2
+ import type { ApiModel } from '../ApiModel.js'
3
+ import type { ExposedEntity } from '../ExposedEntity.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
+ import { observed } from '../../decorators/observed.js'
6
+
7
+ export type MatchResourceAttributeOperator =
8
+ | 'equal'
9
+ | 'notEqual'
10
+ | 'startsWith'
11
+ | 'endsWith'
12
+ | 'contains'
13
+ | 'greaterThan'
14
+ | 'lessThan'
15
+ | 'greaterThanOrEqual'
16
+ | 'lessThanOrEqual'
17
+
18
+ /**
19
+ * Grants access based on a static value within the resource itself.
20
+ * Example: Allow Read behavior if status == 'published'.
21
+ */
22
+ export interface MatchResourceAttributeAccessRuleSchema extends AccessRuleSchema {
23
+ type: 'matchResourceAttribute'
24
+
25
+ /**
26
+ * The name of the attribute on the resource to check.
27
+ */
28
+ attribute: string
29
+
30
+ /**
31
+ * The static value to match against the resource's attribute.
32
+ */
33
+ value: string | number | boolean
34
+
35
+ /**
36
+ * The operator to use when comparing the resource's attribute to the given value.
37
+ * Defaults to 'equal'.
38
+ */
39
+ operator?: MatchResourceAttributeOperator
40
+ }
41
+
42
+ /**
43
+ * Grants access based on a static value within the resource itself.
44
+ * Example: Allow Read behavior if status == 'published'.
45
+ */
46
+ export class MatchResourceAttributeAccessRule extends AccessRule implements MatchResourceAttributeAccessRuleSchema {
47
+ override readonly type: 'matchResourceAttribute'
48
+
49
+ @observed() accessor attribute: string
50
+ @observed() accessor value: string | number | boolean
51
+ @observed() accessor operator: MatchResourceAttributeOperator
52
+
53
+ constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchResourceAttributeAccessRuleSchema> = {}) {
54
+ super(
55
+ parent,
56
+ {
57
+ list: AccessRuleExecutionPhase.FETCH,
58
+ search: AccessRuleExecutionPhase.FETCH,
59
+ read: AccessRuleExecutionPhase.POST_FETCH,
60
+ update: AccessRuleExecutionPhase.POST_FETCH,
61
+ delete: AccessRuleExecutionPhase.POST_FETCH,
62
+ },
63
+ state
64
+ )
65
+ this.type = 'matchResourceAttribute'
66
+ this.attribute = state.attribute ?? ''
67
+ this.value = state.value ?? ''
68
+ this.operator = state.operator ?? 'equal'
69
+ }
70
+
71
+ override toJSON(): MatchResourceAttributeAccessRuleSchema {
72
+ const json: MatchResourceAttributeAccessRuleSchema = {
73
+ ...(super.toJSON() as MatchResourceAttributeAccessRuleSchema),
74
+ attribute: this.attribute,
75
+ value: this.value,
76
+ }
77
+ if (this.operator) {
78
+ json.operator = this.operator
79
+ }
80
+ return json
81
+ }
82
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
  import { observed } from '../../decorators/observed.js'
6
6
 
7
7
  /**
@@ -42,7 +42,17 @@ export class MatchResourceOwnerAccessRule extends AccessRule implements MatchRes
42
42
  @observed() accessor target: 'property' | 'user-entity'
43
43
 
44
44
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchResourceOwnerAccessRuleSchema> = {}) {
45
- super(parent, state)
45
+ super(
46
+ parent,
47
+ {
48
+ list: AccessRuleExecutionPhase.FETCH,
49
+ search: AccessRuleExecutionPhase.FETCH,
50
+ read: AccessRuleExecutionPhase.POST_FETCH,
51
+ update: AccessRuleExecutionPhase.POST_FETCH,
52
+ delete: AccessRuleExecutionPhase.POST_FETCH,
53
+ },
54
+ state
55
+ )
46
56
  this.type = 'matchResourceOwner'
47
57
  this.property = state.property
48
58
  this.target = state.target ?? 'property'
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
  import { observed } from '../../decorators/observed.js'
6
6
 
7
7
  /**
@@ -31,7 +31,18 @@ export class MatchUserPropertyAccessRule extends AccessRule implements MatchUser
31
31
  @observed() accessor value: string
32
32
 
33
33
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserPropertyAccessRuleSchema> = {}) {
34
- super(parent, state)
34
+ super(
35
+ parent,
36
+ {
37
+ list: AccessRuleExecutionPhase.PRE_FETCH,
38
+ create: AccessRuleExecutionPhase.PRE_FETCH,
39
+ search: AccessRuleExecutionPhase.PRE_FETCH,
40
+ read: AccessRuleExecutionPhase.PRE_FETCH,
41
+ update: AccessRuleExecutionPhase.PRE_FETCH,
42
+ delete: AccessRuleExecutionPhase.PRE_FETCH,
43
+ },
44
+ state
45
+ )
35
46
  this.type = 'matchUserProperty'
36
47
  this.property = state.property ?? ''
37
48
  this.value = state.value ?? ''
@@ -1,7 +1,7 @@
1
1
  import type { Action } from '../actions/Action.js'
2
2
  import type { ApiModel } from '../ApiModel.js'
3
3
  import type { ExposedEntity } from '../ExposedEntity.js'
4
- import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
4
+ import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
5
5
  import { observed, toRaw } from '../../decorators/observed.js'
6
6
 
7
7
  /**
@@ -32,7 +32,18 @@ export class MatchUserRoleAccessRule extends AccessRule implements MatchUserRole
32
32
  @observed({ deep: true }) accessor role: string[]
33
33
 
34
34
  constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserRoleAccessRuleSchema> = {}) {
35
- super(parent, state)
35
+ super(
36
+ parent,
37
+ {
38
+ list: AccessRuleExecutionPhase.PRE_FETCH,
39
+ create: AccessRuleExecutionPhase.PRE_FETCH,
40
+ search: AccessRuleExecutionPhase.PRE_FETCH,
41
+ read: AccessRuleExecutionPhase.PRE_FETCH,
42
+ update: AccessRuleExecutionPhase.PRE_FETCH,
43
+ delete: AccessRuleExecutionPhase.PRE_FETCH,
44
+ },
45
+ state
46
+ )
36
47
  this.type = 'matchUserRole'
37
48
  this.role = state.role ? [...state.role] : []
38
49
  }