@api-client/core 0.20.1 → 0.20.3

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.
@@ -10,11 +10,10 @@ test.group('RuntimeApiModel', () => {
10
10
  test('initializes from schema', ({ assert }) => {
11
11
  const domain = new DataDomain({ info: { version: '1.0.0' } })
12
12
  const model = domain.addModel({ info: { name: 'Test Model' } })
13
- const entity = model.addEntity({ key: 'ent-1', info: { name: 'User' } })
13
+ const entity = model.addEntity({ info: { name: 'User' } })
14
14
 
15
15
  const baseModel = new ApiModel(
16
16
  {
17
- key: 'api-1',
18
17
  info: { name: 'Test API' },
19
18
  },
20
19
  domain
@@ -37,13 +36,15 @@ test.group('RuntimeApiModel', () => {
37
36
 
38
37
  test('resolves path with matchit', ({ assert }) => {
39
38
  const domain = new DataDomain({ info: { version: '1.0.0' } })
39
+ const model = domain.addModel({ info: { name: 'Test Model' } })
40
+ const entity = model.addEntity({ info: { name: 'User' } })
40
41
  const schema = {
41
42
  key: 'api-1',
42
43
  info: { name: 'Test API' },
43
44
  exposes: [
44
45
  {
45
46
  key: 'expose-1',
46
- entity: { key: 'ent-1', type: 'association' },
47
+ entity: { key: entity.key, domain: domain.key },
47
48
  actions: [
48
49
  { kind: 'read', type: 'crud' },
49
50
  { kind: 'list', type: 'crud' },
@@ -92,7 +93,7 @@ test.group('RuntimeApiModel', () => {
92
93
  assert.isUndefined(getResult)
93
94
  }).tags(['@modeling', '@runtime'])
94
95
 
95
- test('returns undefined when entity or action is missing', ({ assert }) => {
96
+ test('throws error when entity or action is missing', ({ assert }) => {
96
97
  const domain = new DataDomain({ info: { version: '1.0.0' } })
97
98
  const schema = {
98
99
  key: 'api-1',
@@ -115,12 +116,14 @@ test.group('RuntimeApiModel', () => {
115
116
  const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
116
117
 
117
118
  // Exposed entity not found
118
- const missingEntityResult = runtimeModel.lookupAction('GET', '/missing-entity')
119
- assert.isUndefined(missingEntityResult)
119
+ assert.throws(() => {
120
+ runtimeModel.lookupAction('GET', '/missing-entity')
121
+ }, 'Missing exposure missing-expose')
120
122
 
121
123
  // Action not found on entity
122
- const missingActionResult = runtimeModel.lookupAction('GET', '/missing-action')
123
- assert.isUndefined(missingActionResult)
124
+ assert.throws(() => {
125
+ runtimeModel.lookupAction('GET', '/missing-action')
126
+ }, 'Missing action read')
124
127
  }).tags(['@modeling', '@runtime'])
125
128
 
126
129
  test('caches user entity and properties based on semantics', ({ assert }) => {
@@ -211,7 +214,7 @@ test.group('RuntimeApiModel', () => {
211
214
  assert.equal(runtimeModel.sessionProperties.size, 0)
212
215
  }).tags(['@modeling', '@runtime'])
213
216
 
214
- test('evaluateAccess - rejects if any mandatory rule fails', async ({ assert }) => {
217
+ test('getEffectiveRules - separates mandatory and permission rules', async ({ assert }) => {
215
218
  const domain = new DataDomain({ info: { version: '1.0.0' } })
216
219
  const baseModel = new ApiModel(
217
220
  { key: 'api-1', accessRule: [{ type: 'allowAuthenticated', mandatory: true }] },
@@ -220,40 +223,41 @@ test.group('RuntimeApiModel', () => {
220
223
  const runtimeModel = new RuntimeApiModel(baseModel.toJSON() as any, domain.toJSON())
221
224
 
222
225
  const mockAction = {
223
- entity: { accessRule: [], parent: undefined },
226
+ exposure: { accessRule: [], parent: undefined },
224
227
  action: { kind: 'read', accessRule: [] },
225
228
  } as any
226
229
 
227
- const evaluator: RuleEvaluator = async (_rule: AccessRule): Promise<boolean> => false // Mandatory rule fails
228
- const result = await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
229
- assert.isFalse(result)
230
+ const rules = runtimeModel.getEffectiveRules(mockAction)
231
+ assert.equal(rules.preFetch.mandatoryRules.length, 1)
232
+ assert.equal(rules.preFetch.mandatoryRules[0].type, 'allowAuthenticated')
233
+ assert.equal(rules.preFetch.permissionRules.length, 0)
230
234
  }).tags(['@modeling', '@runtime', '@authorization'])
231
235
 
232
- test('evaluateAccess - rejects by default if no rules hit', async ({ assert }) => {
236
+ test('getEffectiveRules - defaults to preFetch phase if none specified', async ({ assert }) => {
233
237
  const domain = new DataDomain({ info: { version: '1.0.0' } })
234
238
  const baseModel = new ApiModel({ key: 'api-1' }, domain)
235
239
  const runtimeModel = new RuntimeApiModel(baseModel.toJSON() as any, domain.toJSON())
236
240
 
237
241
  const mockAction = {
238
- entity: {
242
+ exposure: {
239
243
  accessRule: [{ type: 'allowPublic', metadata: { read: AccessRuleExecutionPhase.PRE_FETCH } }],
240
244
  parent: undefined,
241
245
  },
242
246
  action: { kind: 'read', accessRule: [] },
243
247
  } as any
244
248
 
245
- const evaluator: RuleEvaluator = async (_rule: AccessRule): Promise<undefined> => undefined // No rules match
246
- const result = await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
247
- assert.isFalse(result)
249
+ const rules = runtimeModel.getEffectiveRules(mockAction)
250
+ assert.equal(rules.preFetch.permissionRules.length, 1)
251
+ assert.equal(rules.preFetch.permissionRules[0].type, 'allowPublic')
248
252
  }).tags(['@modeling', '@runtime', '@authorization'])
249
253
 
250
- test('evaluateAccess - evaluates from most specific to most general in permission phase', async ({ assert }) => {
254
+ test('getEffectiveRules - orders from most specific to most general', async ({ assert }) => {
251
255
  const domain = new DataDomain({ info: { version: '1.0.0' } })
252
- const baseModel = new ApiModel({ key: 'api-1', accessRule: [{ type: 'allowAuthenticated' }] }, domain)
256
+ const baseModel = new ApiModel({ accessRule: [{ type: 'allowAuthenticated' }] }, domain)
253
257
  const runtimeModel = new RuntimeApiModel(baseModel.toJSON() as any, domain.toJSON())
254
258
 
255
259
  const mockAction = {
256
- entity: {
260
+ exposure: {
257
261
  accessRule: [{ type: 'allowPublic', metadata: { read: AccessRuleExecutionPhase.PRE_FETCH } }],
258
262
  parent: undefined,
259
263
  },
@@ -263,77 +267,19 @@ test.group('RuntimeApiModel', () => {
263
267
  },
264
268
  } as any
265
269
 
266
- const evaluatedOrder: string[] = []
267
- const evaluator: RuleEvaluator = async (rule: AccessRule): Promise<undefined> => {
268
- evaluatedOrder.push(rule.type)
269
- return undefined // Continue to next rule
270
- }
271
-
272
- await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
273
- assert.deepEqual(evaluatedOrder, ['matchEmailDomain', 'allowPublic', 'allowAuthenticated'])
274
- }).tags(['@modeling', '@runtime', '@authorization'])
275
-
276
- test('evaluateAccess - short-circuits on explicit allow or deny in permission phase', async ({ assert }) => {
277
- const domain = new DataDomain({ info: { version: '1.0.0' } })
278
- const baseModel = new ApiModel({ key: 'api-1', accessRule: [{ type: 'allowAuthenticated' }] }, domain)
279
- const runtimeModel = new RuntimeApiModel(baseModel.toJSON() as any, domain.toJSON())
280
-
281
- const mockAction = {
282
- entity: {
283
- accessRule: [{ type: 'allowPublic', metadata: { read: AccessRuleExecutionPhase.PRE_FETCH } }],
284
- parent: undefined,
285
- },
286
- action: {
287
- kind: 'read',
288
- accessRule: [{ type: 'matchEmailDomain', metadata: { read: AccessRuleExecutionPhase.PRE_FETCH } }],
289
- },
290
- } as any
291
-
292
- const evaluatedOrder: string[] = []
293
- const evaluator = async (rule: any) => {
294
- evaluatedOrder.push(rule.type)
295
- if (rule.type === 'allowPublic') return true
296
- return undefined
297
- }
298
-
299
- const result = await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
300
- assert.isTrue(result)
301
- // Should not reach the api-rule
302
- assert.deepEqual(evaluatedOrder, ['matchEmailDomain', 'allowPublic'])
303
- }).tags(['@modeling', '@runtime', '@authorization'])
304
-
305
- test('evaluateAccess - passes mandatory phase and uses permission phase result', async ({ assert }) => {
306
- const domain = new DataDomain({ info: { version: '1.0.0' } })
307
- const baseModel = new ApiModel(
308
- { key: 'api-1', accessRule: [{ type: 'allowAuthenticated', mandatory: true }] },
309
- domain
270
+ const rules = runtimeModel.getEffectiveRules(mockAction)
271
+ assert.equal(rules.preFetch.permissionRules.length, 3)
272
+ assert.deepEqual(
273
+ rules.preFetch.permissionRules.map((r) => r.type),
274
+ ['matchEmailDomain', 'allowPublic', 'allowAuthenticated']
310
275
  )
311
- const runtimeModel = new RuntimeApiModel(baseModel.toJSON() as any, domain.toJSON())
312
-
313
- const mockAction = {
314
- entity: {
315
- accessRule: [{ type: 'allowPublic', metadata: { read: AccessRuleExecutionPhase.PRE_FETCH } }],
316
- parent: undefined,
317
- },
318
- action: { kind: 'read', accessRule: [] },
319
- } as any
320
-
321
- const evaluator = async (rule: any) => {
322
- if (rule.mandatory) return true // passes mandatory check
323
- if (rule.type === 'allowPublic') return false // explicitly denies
324
- return undefined
325
- }
326
-
327
- const result = await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
328
- assert.isFalse(result) // Denied by permission rule
329
276
  }).tags(['@modeling', '@runtime', '@authorization'])
330
277
 
331
- test('evaluateAccess - shadows parent rules of the same type', async ({ assert }) => {
278
+ test('getEffectiveRules - shadows parent rules of the same type', async ({ assert }) => {
332
279
  const domain = new DataDomain({ info: { version: '1.0.0' } })
333
280
  const model = domain.addModel({ key: 'mod-1' })
334
281
  const entity = model.addEntity({ key: 'ent-1' })
335
282
 
336
- // Create base model with exposing and actions directly in schema
337
283
  const baseModel = new ApiModel(
338
284
  {
339
285
  key: 'api-1',
@@ -361,19 +307,6 @@ test.group('RuntimeApiModel', () => {
361
307
  domain
362
308
  )
363
309
 
364
- // Hierarchy will be:
365
- // Action: allowAuthenticated (mandatory)
366
- // Entity: allowPublic, matchEmailDomain (mandatory)
367
- // API: allowAuthenticated, matchEmailDomain
368
-
369
- // Effective Rules (with shadowing):
370
- // Action's allowAuthenticated (mandatory) shadows API's allowAuthenticated
371
- // Entity's matchEmailDomain (mandatory) shadows API's matchEmailDomain
372
- // Entity's allowPublic is kept
373
-
374
- // Mandatory: allowAuthenticated, matchEmailDomain
375
- // Permission: allowPublic
376
-
377
310
  const schema: RuntimeApiModelSchema = {
378
311
  ...baseModel.toJSON(),
379
312
  routingMap: {
@@ -384,16 +317,15 @@ test.group('RuntimeApiModel', () => {
384
317
  const runtimeModel = new RuntimeApiModel(schema, domain.toJSON())
385
318
  const mockAction = runtimeModel.lookupAction('GET', '/test')!
386
319
 
387
- const evaluatedOrder: string[] = []
388
- const evaluator: RuleEvaluator = async (rule: AccessRule): Promise<boolean | undefined> => {
389
- evaluatedOrder.push(`${rule.type}${rule.mandatory ? '-mandatory' : ''}`)
390
- if (rule.mandatory) return true
391
- return undefined
392
- }
393
-
394
- await runtimeModel.evaluateAccess(mockAction, evaluator, AccessRuleExecutionPhase.PRE_FETCH)
320
+ const rules = runtimeModel.getEffectiveRules(mockAction)
395
321
 
396
- // Check that we only evaluate the shadowed rules, exactly once per phase
397
- assert.deepEqual(evaluatedOrder, ['allowAuthenticated-mandatory', 'matchEmailDomain-mandatory', 'allowPublic'])
322
+ assert.deepEqual(
323
+ rules.preFetch.mandatoryRules.map((r) => r.type),
324
+ ['allowAuthenticated', 'matchEmailDomain']
325
+ )
326
+ assert.deepEqual(
327
+ rules.preFetch.permissionRules.map((r) => r.type),
328
+ ['allowPublic']
329
+ )
398
330
  }).tags(['@modeling', '@runtime', '@authorization'])
399
331
  })
@@ -5,8 +5,6 @@ import {
5
5
  isPropertySemantic,
6
6
  SemanticCategory,
7
7
  SemanticScope,
8
- SemanticTiming,
9
- SemanticOperation,
10
8
  } from '../../../src/modeling/Semantics.js'
11
9
 
12
10
  test.group('ClientIPAddress Semantic', () => {
@@ -48,24 +46,4 @@ test.group('ClientIPAddress Semantic', () => {
48
46
  const semantic = DataSemantics[SemanticType.ClientIPAddress]
49
47
  assert.isFalse(semantic.hasConfig)
50
48
  })
51
-
52
- test('should have correct runtime configuration', ({ assert }) => {
53
- const semantic = DataSemantics[SemanticType.ClientIPAddress]
54
- const runtime = semantic.runtime
55
-
56
- assert.equal(runtime.timing, SemanticTiming.Before)
57
- assert.deepEqual(runtime.operations, [SemanticOperation.Create, SemanticOperation.Update])
58
- assert.equal(runtime.priority, 95) // Low priority, populate after other validations
59
- assert.equal(runtime.timeoutMs, 100) // Very fast operation
60
- })
61
-
62
- test('should have correct execution conditions', ({ assert }) => {
63
- const semantic = DataSemantics[SemanticType.ClientIPAddress]
64
- const conditions = semantic.runtime.conditions
65
-
66
- assert.isDefined(conditions)
67
- assert.lengthOf(conditions!, 1)
68
- assert.equal(conditions![0].expression, 'entity[semantics.ClientIPAddress] == null')
69
- assert.equal(conditions![0].description, 'Only set IP address if not already provided')
70
- })
71
49
  })
@@ -3,8 +3,6 @@ import {
3
3
  SemanticType,
4
4
  SemanticScope,
5
5
  SemanticCategory,
6
- SemanticTiming,
7
- SemanticOperation,
8
6
  isEntitySemantic,
9
7
  isPropertySemantic,
10
8
  isAssociationSemantic,
@@ -52,10 +50,6 @@ test.group('Semantics', () => {
52
50
  scope: SemanticScope.Entity,
53
51
  category: SemanticCategory.Identity,
54
52
  hasConfig: false,
55
- runtime: {
56
- timing: SemanticTiming.None,
57
- operations: [],
58
- },
59
53
  }
60
54
  const propertySemantic: PropertySemantic = {
61
55
  id: SemanticType.CreatedTimestamp,
@@ -65,11 +59,6 @@ test.group('Semantics', () => {
65
59
  category: SemanticCategory.Lifecycle,
66
60
  hasConfig: false,
67
61
  applicableDataTypes: ['datetime'],
68
- runtime: {
69
- timing: SemanticTiming.Before,
70
- operations: [SemanticOperation.Create],
71
- priority: 90,
72
- },
73
62
  }
74
63
  const associationSemantic: AssociationSemantic = {
75
64
  id: SemanticType.ResourceOwnerIdentifier,
@@ -78,18 +67,6 @@ test.group('Semantics', () => {
78
67
  scope: SemanticScope.Association,
79
68
  category: SemanticCategory.Identity,
80
69
  hasConfig: false,
81
- runtime: {
82
- timing: SemanticTiming.Before,
83
- operations: [
84
- SemanticOperation.Create,
85
- SemanticOperation.Read,
86
- SemanticOperation.Update,
87
- SemanticOperation.Delete,
88
- SemanticOperation.List,
89
- ],
90
- priority: 5,
91
- canDisable: false,
92
- },
93
70
  }
94
71
 
95
72
  assert.isTrue(isEntitySemantic(entitySemantic))
@@ -105,10 +82,6 @@ test.group('Semantics', () => {
105
82
  scope: SemanticScope.Entity,
106
83
  category: SemanticCategory.Identity,
107
84
  hasConfig: false,
108
- runtime: {
109
- timing: SemanticTiming.None,
110
- operations: [],
111
- },
112
85
  }
113
86
  const propertySemantic: PropertySemantic = {
114
87
  id: SemanticType.CreatedTimestamp,
@@ -118,11 +91,6 @@ test.group('Semantics', () => {
118
91
  category: SemanticCategory.Lifecycle,
119
92
  hasConfig: false,
120
93
  applicableDataTypes: ['datetime'],
121
- runtime: {
122
- timing: SemanticTiming.Before,
123
- operations: [SemanticOperation.Create],
124
- priority: 90,
125
- },
126
94
  }
127
95
  const associationSemantic: AssociationSemantic = {
128
96
  id: SemanticType.ResourceOwnerIdentifier,
@@ -131,18 +99,6 @@ test.group('Semantics', () => {
131
99
  scope: SemanticScope.Association,
132
100
  category: SemanticCategory.Identity,
133
101
  hasConfig: false,
134
- runtime: {
135
- timing: SemanticTiming.Before,
136
- operations: [
137
- SemanticOperation.Create,
138
- SemanticOperation.Read,
139
- SemanticOperation.Update,
140
- SemanticOperation.Delete,
141
- SemanticOperation.List,
142
- ],
143
- priority: 5,
144
- canDisable: false,
145
- },
146
102
  }
147
103
 
148
104
  assert.isFalse(isPropertySemantic(entitySemantic))
@@ -158,10 +114,6 @@ test.group('Semantics', () => {
158
114
  scope: SemanticScope.Entity,
159
115
  category: SemanticCategory.Identity,
160
116
  hasConfig: false,
161
- runtime: {
162
- timing: SemanticTiming.None,
163
- operations: [],
164
- },
165
117
  }
166
118
  const propertySemantic: PropertySemantic = {
167
119
  id: SemanticType.CreatedTimestamp,
@@ -171,11 +123,6 @@ test.group('Semantics', () => {
171
123
  category: SemanticCategory.Lifecycle,
172
124
  hasConfig: false,
173
125
  applicableDataTypes: ['datetime'],
174
- runtime: {
175
- timing: SemanticTiming.Before,
176
- operations: [SemanticOperation.Create],
177
- priority: 90,
178
- },
179
126
  }
180
127
  const associationSemantic: AssociationSemantic = {
181
128
  id: SemanticType.ResourceOwnerIdentifier,
@@ -184,18 +131,6 @@ test.group('Semantics', () => {
184
131
  scope: SemanticScope.Association,
185
132
  category: SemanticCategory.Identity,
186
133
  hasConfig: false,
187
- runtime: {
188
- timing: SemanticTiming.Before,
189
- operations: [
190
- SemanticOperation.Create,
191
- SemanticOperation.Read,
192
- SemanticOperation.Update,
193
- SemanticOperation.Delete,
194
- SemanticOperation.List,
195
- ],
196
- priority: 5,
197
- canDisable: false,
198
- },
199
134
  }
200
135
 
201
136
  assert.isFalse(isAssociationSemantic(entitySemantic))
@@ -5,8 +5,6 @@ import {
5
5
  isPropertySemantic,
6
6
  SemanticCategory,
7
7
  SemanticScope,
8
- SemanticTiming,
9
- SemanticOperation,
10
8
  } from '../../../src/modeling/Semantics.js'
11
9
 
12
10
  test.group('Username Semantic', () => {
@@ -48,34 +46,4 @@ test.group('Username Semantic', () => {
48
46
  const semantic = DataSemantics[SemanticType.Username]
49
47
  assert.isFalse(semantic.hasConfig)
50
48
  })
51
-
52
- test('should not be disableable for security reasons', ({ assert }) => {
53
- const semantic = DataSemantics[SemanticType.Username]
54
- assert.isFalse(semantic.runtime.canDisable)
55
- })
56
-
57
- test('should have correct runtime configuration', ({ assert }) => {
58
- const semantic = DataSemantics[SemanticType.Username]
59
- const runtime = semantic.runtime
60
-
61
- assert.equal(runtime.timing, SemanticTiming.Before)
62
- assert.deepEqual(runtime.operations, [SemanticOperation.Create, SemanticOperation.Update, SemanticOperation.Read])
63
- assert.equal(runtime.priority, 15) // High priority for authentication
64
- assert.equal(runtime.timeoutMs, 100) // Fast operation
65
- })
66
-
67
- test('should have higher priority than general user properties but lower than password', ({ assert }) => {
68
- const usernameSemantic = DataSemantics[SemanticType.Username]
69
- const passwordSemantic = DataSemantics[SemanticType.Password]
70
- const userRoleSemantic = DataSemantics[SemanticType.UserRole]
71
-
72
- const usernameP = usernameSemantic.runtime.priority ?? 100
73
- const passwordP = passwordSemantic.runtime.priority ?? 100
74
- const userRoleP = userRoleSemantic.runtime.priority ?? 100
75
-
76
- // Username should have lower priority number (higher priority) than UserRole
77
- assert.isAtMost(usernameP, userRoleP)
78
- // But higher priority number (lower priority) than Password
79
- assert.isAtLeast(usernameP, passwordP)
80
- })
81
49
  })
@@ -1,84 +0,0 @@
1
- import { Jexl } from '@pawel-up/jexl/Jexl.js';
2
- import type { DomainEntity } from '../../modeling/DomainEntity.js';
3
- import { type AppliedDataSemantic, type SemanticCondition, SemanticOperation, SemanticType } from '../../modeling/Semantics.js';
4
- import type { TransformFunction } from '@pawel-up/jexl';
5
- /**
6
- * Get or create a JEXL instance with optional custom transforms
7
- */
8
- export declare function getJexlInstance(transforms?: Record<string, TransformFunction>): Jexl;
9
- /**
10
- * Context object passed to semantic condition evaluation
11
- */
12
- export interface SemanticExecutionContext {
13
- /**
14
- * The entity data being processed
15
- */
16
- entity: Record<string, unknown>;
17
- /**
18
- * Current user context (if authenticated)
19
- */
20
- user?: {
21
- id?: string;
22
- authenticated?: boolean;
23
- roles?: string[];
24
- [key: string]: unknown;
25
- };
26
- /**
27
- * The current database operation
28
- */
29
- operation: SemanticOperation;
30
- /**
31
- * Applied semantics with their field mappings
32
- */
33
- appliedSemantics: AppliedDataSemantic[];
34
- /**
35
- * Configuration for the current semantic being evaluated
36
- */
37
- config?: Record<string, unknown>;
38
- }
39
- /**
40
- * Builds a semantics object for JEXL evaluation that maps semantic types to actual field names.
41
- *
42
- * The resulting map should be used in JEXL expressions to reference semantic fields.
43
- *
44
- * @returns A map where keys are semantic type identifiers and values are the field names.
45
- *
46
- * @example
47
- * const semanticFieldMap = buildSemanticFieldMap(entity);
48
- * const result = await jexl.eval("semantics.CreatedTimestamp == null", {
49
- * semantics: semanticFieldMap,
50
- * ...
51
- * }
52
- */
53
- export declare function buildSemanticFieldMap(entity: DomainEntity): Record<string, string>;
54
- /**
55
- * Evaluates a semantic condition against the execution context
56
- * In a real implementation, this would use JEXL to evaluate the expression
57
- *
58
- * @param condition The semantic condition to evaluate
59
- * @param context The execution context containing entity data, user info, etc.
60
- * @param semanticFieldMap A map of semantic field names to their actual field names.
61
- * Use the `buildSemanticFieldMap()` function to create this map.
62
- * @param jexl Optional JEXL instance to use for evaluation, defaults to a cached instance.
63
- * @returns A promise that resolves to true if the condition is met, false otherwise
64
- */
65
- export declare function evaluateSemanticCondition(condition: SemanticCondition, context: SemanticExecutionContext, semanticFieldMap: Record<string, string>, jexl?: Jexl): Promise<boolean>;
66
- /**
67
- * Check if semantic should execute based on all its conditions
68
- *
69
- * @param semantic The semantic to check
70
- * @param context The execution context containing entity data, user info, etc.
71
- * @param semanticFieldMap A map of semantic field names to their actual field names.
72
- * Use the `buildSemanticFieldMap()` function to create this map.
73
- * @return A promise that resolves to true if all conditions are met, false otherwise
74
- */
75
- export declare function shouldSemanticExecute(semantic: AppliedDataSemantic, context: SemanticExecutionContext, semanticFieldMap: Record<string, string>): Promise<boolean>;
76
- /**
77
- * Helper to get the actual field name for a semantic type
78
- */
79
- export declare function getFieldNameForSemantic(semanticType: SemanticType, semanticFieldMap: Record<string, string>): string | undefined;
80
- /**
81
- * Validates that all semantic references in conditions are available
82
- */
83
- export declare function validateSemanticConditions(semantic: AppliedDataSemantic, semanticFieldMap: Record<string, string>): string[];
84
- //# sourceMappingURL=Semantics.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Semantics.d.ts","sourceRoot":"","sources":["../../../../src/runtime/modeling/Semantics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAA;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,YAAY,EACb,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAOvD;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,IAAI,CAcpF;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;QAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB,CAAA;IACD;;OAEG;IACH,SAAS,EAAE,iBAAiB,CAAA;IAC5B;;OAEG;IACH,gBAAgB,EAAE,mBAAmB,EAAE,CAAA;IACvC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBlF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,iBAAiB,EAC5B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,IAAI,GAAE,IAAwB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,YAAY,EAC1B,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,MAAM,GAAG,SAAS,CAGpB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,mBAAmB,EAC7B,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,MAAM,EAAE,CAuBV"}
@@ -1,124 +0,0 @@
1
- import { Jexl } from '@pawel-up/jexl/Jexl.js';
2
- import { DataSemantics, } from '../../modeling/Semantics.js';
3
- /**
4
- * Cache for JEXL instances to avoid recreation overhead
5
- */
6
- const jexlCache = new Map();
7
- /**
8
- * Get or create a JEXL instance with optional custom transforms
9
- */
10
- export function getJexlInstance(transforms) {
11
- const cacheKey = transforms ? JSON.stringify(Object.keys(transforms).sort()) : 'default';
12
- if (!jexlCache.has(cacheKey)) {
13
- const jexl = new Jexl();
14
- if (transforms) {
15
- Object.entries(transforms).forEach(([name, fn]) => {
16
- jexl.addTransform(name, fn);
17
- });
18
- }
19
- jexlCache.set(cacheKey, jexl);
20
- }
21
- return jexlCache.get(cacheKey);
22
- }
23
- /**
24
- * Builds a semantics object for JEXL evaluation that maps semantic types to actual field names.
25
- *
26
- * The resulting map should be used in JEXL expressions to reference semantic fields.
27
- *
28
- * @returns A map where keys are semantic type identifiers and values are the field names.
29
- *
30
- * @example
31
- * const semanticFieldMap = buildSemanticFieldMap(entity);
32
- * const result = await jexl.eval("semantics.CreatedTimestamp == null", {
33
- * semantics: semanticFieldMap,
34
- * ...
35
- * }
36
- */
37
- export function buildSemanticFieldMap(entity) {
38
- const semanticFieldMap = {};
39
- for (const property of entity.properties) {
40
- if (!property.info.name) {
41
- continue; // Skip properties without a name
42
- }
43
- for (const semantic of property.semantics) {
44
- // We use the truncated `semantic.id` as the key, e.g. 'Password' so that it is possible to do something like:
45
- // `entity[semantics.Password] != null && !entity[semantics.Password].startsWith('$')`
46
- // Where the `semantics` object is the object returned by this function and
47
- // passed to the JEXL context.
48
- const semanticKey = semantic.id.replace('Semantic#', '');
49
- semanticFieldMap[semanticKey] = property.info.name;
50
- }
51
- }
52
- return semanticFieldMap;
53
- }
54
- /**
55
- * Evaluates a semantic condition against the execution context
56
- * In a real implementation, this would use JEXL to evaluate the expression
57
- *
58
- * @param condition The semantic condition to evaluate
59
- * @param context The execution context containing entity data, user info, etc.
60
- * @param semanticFieldMap A map of semantic field names to their actual field names.
61
- * Use the `buildSemanticFieldMap()` function to create this map.
62
- * @param jexl Optional JEXL instance to use for evaluation, defaults to a cached instance.
63
- * @returns A promise that resolves to true if the condition is met, false otherwise
64
- */
65
- export function evaluateSemanticCondition(condition, context, semanticFieldMap, jexl = getJexlInstance()) {
66
- // Build the evaluation context
67
- const evalContext = {
68
- entity: context.entity,
69
- user: context.user,
70
- operation: context.operation,
71
- config: context.config,
72
- semantics: semanticFieldMap,
73
- };
74
- return jexl.eval(condition.expression, evalContext);
75
- }
76
- /**
77
- * Check if semantic should execute based on all its conditions
78
- *
79
- * @param semantic The semantic to check
80
- * @param context The execution context containing entity data, user info, etc.
81
- * @param semanticFieldMap A map of semantic field names to their actual field names.
82
- * Use the `buildSemanticFieldMap()` function to create this map.
83
- * @return A promise that resolves to true if all conditions are met, false otherwise
84
- */
85
- export async function shouldSemanticExecute(semantic, context, semanticFieldMap) {
86
- const definition = DataSemantics[semantic.id];
87
- const conditions = definition?.runtime?.conditions;
88
- if (!conditions || conditions.length === 0) {
89
- return true;
90
- }
91
- const results = await Promise.all(conditions.map((condition) => evaluateSemanticCondition(condition, context, semanticFieldMap)));
92
- // All conditions must evaluate to true
93
- return results.every((result) => result === true);
94
- }
95
- /**
96
- * Helper to get the actual field name for a semantic type
97
- */
98
- export function getFieldNameForSemantic(semanticType, semanticFieldMap) {
99
- const semanticKey = semanticType.replace('Semantic#', '');
100
- return semanticFieldMap[semanticKey];
101
- }
102
- /**
103
- * Validates that all semantic references in conditions are available
104
- */
105
- export function validateSemanticConditions(semantic, semanticFieldMap) {
106
- const definition = DataSemantics[semantic.id];
107
- const conditions = definition?.runtime?.conditions || [];
108
- const errors = [];
109
- conditions.forEach((condition, index) => {
110
- // Extract semantic references from the condition expression
111
- // Look for patterns like "semantics.CreatedTimestamp" or "entity[semantics.Password]"
112
- const semanticMatches = condition.expression.match(/semantics\.(\w+)/g);
113
- if (semanticMatches) {
114
- semanticMatches.forEach((match) => {
115
- const semanticKey = match.replace('semantics.', '');
116
- if (!semanticFieldMap[semanticKey]) {
117
- errors.push(`Condition ${index + 1} references semantic "${semanticKey}" but no field with that semantic was found`);
118
- }
119
- });
120
- }
121
- });
122
- return errors;
123
- }
124
- //# sourceMappingURL=Semantics.js.map