@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.
- package/build/src/modeling/RuntimeApiModel.d.ts +14 -18
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
- package/build/src/modeling/RuntimeApiModel.js +14 -52
- package/build/src/modeling/RuntimeApiModel.js.map +1 -1
- package/build/src/modeling/Semantics.d.ts +0 -252
- package/build/src/modeling/Semantics.d.ts.map +1 -1
- package/build/src/modeling/Semantics.js +0 -356
- package/build/src/modeling/Semantics.js.map +1 -1
- package/build/src/modeling/ai/tools/Semantic.tools.d.ts +0 -3
- package/build/src/modeling/ai/tools/Semantic.tools.d.ts.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/modeling/RuntimeApiModel.ts +19 -61
- package/src/modeling/Semantics.ts +40 -520
- package/tests/unit/modeling/RuntimeApiModel.spec.ts +40 -108
- package/tests/unit/modeling/client_ip_address_semantic.spec.ts +0 -22
- package/tests/unit/modeling/semantics.spec.ts +0 -65
- package/tests/unit/modeling/username_semantic.spec.ts +0 -32
- package/build/src/runtime/modeling/Semantics.d.ts +0 -84
- package/build/src/runtime/modeling/Semantics.d.ts.map +0 -1
- package/build/src/runtime/modeling/Semantics.js +0 -124
- package/build/src/runtime/modeling/Semantics.js.map +0 -1
- package/src/runtime/modeling/Semantics.ts +0 -196
- package/tests/unit/modeling/semantic_runtime.spec.ts +0 -113
|
@@ -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({
|
|
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:
|
|
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('
|
|
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
|
-
|
|
119
|
-
|
|
119
|
+
assert.throws(() => {
|
|
120
|
+
runtimeModel.lookupAction('GET', '/missing-entity')
|
|
121
|
+
}, 'Missing exposure missing-expose')
|
|
120
122
|
|
|
121
123
|
// Action not found on entity
|
|
122
|
-
|
|
123
|
-
|
|
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('
|
|
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
|
-
|
|
226
|
+
exposure: { accessRule: [], parent: undefined },
|
|
224
227
|
action: { kind: 'read', accessRule: [] },
|
|
225
228
|
} as any
|
|
226
229
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
assert.
|
|
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('
|
|
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
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
assert.
|
|
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('
|
|
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({
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
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
|