@api-client/core 0.18.25 → 0.18.27

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 (44) hide show
  1. package/build/src/modeling/Semantics.d.ts +193 -0
  2. package/build/src/modeling/Semantics.d.ts.map +1 -1
  3. package/build/src/modeling/Semantics.js +134 -0
  4. package/build/src/modeling/Semantics.js.map +1 -1
  5. package/build/src/modeling/helpers/Intelisense.d.ts +7 -7
  6. package/build/src/modeling/helpers/Intelisense.d.ts.map +1 -1
  7. package/build/src/modeling/helpers/Intelisense.js.map +1 -1
  8. package/build/src/modeling/templates/meta/blog-publishing-platform.json +1 -1
  9. package/build/src/modeling/templates/meta/financial-services-platform.json +1 -1
  10. package/build/src/modeling/templates/meta/index.d.ts +1 -1
  11. package/build/src/modeling/templates/meta/index.js +1 -1
  12. package/build/src/modeling/templates/meta/index.js.map +1 -1
  13. package/build/src/modeling/templates/meta/iot-smart-home-platform.json +1 -1
  14. package/build/src/modeling/templates/verticals/business-services/financial-services-domain.d.ts.map +1 -1
  15. package/build/src/modeling/templates/verticals/business-services/financial-services-domain.js +249 -63
  16. package/build/src/modeling/templates/verticals/business-services/financial-services-domain.js.map +1 -1
  17. package/build/src/modeling/templates/verticals/technology-media/blog-domain.d.ts.map +1 -1
  18. package/build/src/modeling/templates/verticals/technology-media/blog-domain.js +17 -9
  19. package/build/src/modeling/templates/verticals/technology-media/blog-domain.js.map +1 -1
  20. package/build/src/modeling/templates/verticals/technology-media/iot-smart-home-domain.d.ts.map +1 -1
  21. package/build/src/modeling/templates/verticals/technology-media/iot-smart-home-domain.js +2 -0
  22. package/build/src/modeling/templates/verticals/technology-media/iot-smart-home-domain.js.map +1 -1
  23. package/build/src/modeling/validation/postgresql.d.ts.map +1 -1
  24. package/build/src/modeling/validation/postgresql.js +0 -1
  25. package/build/src/modeling/validation/postgresql.js.map +1 -1
  26. package/build/src/runtime/modeling/Semantics.d.ts +84 -0
  27. package/build/src/runtime/modeling/Semantics.d.ts.map +1 -0
  28. package/build/src/runtime/modeling/Semantics.js +124 -0
  29. package/build/src/runtime/modeling/Semantics.js.map +1 -0
  30. package/build/tsconfig.tsbuildinfo +1 -1
  31. package/data/models/example-generator-api.json +14 -14
  32. package/package.json +1 -1
  33. package/src/modeling/Semantics.ts +262 -0
  34. package/src/modeling/helpers/Intelisense.ts +7 -7
  35. package/src/modeling/templates/meta/blog-publishing-platform.json +1 -1
  36. package/src/modeling/templates/meta/financial-services-platform.json +1 -1
  37. package/src/modeling/templates/meta/iot-smart-home-platform.json +1 -1
  38. package/src/modeling/templates/verticals/business-services/financial-services-domain.ts +286 -65
  39. package/src/modeling/templates/verticals/technology-media/blog-domain.ts +17 -9
  40. package/src/modeling/templates/verticals/technology-media/iot-smart-home-domain.ts +2 -0
  41. package/src/modeling/validation/postgresql.ts +0 -1
  42. package/src/runtime/modeling/Semantics.ts +196 -0
  43. package/tests/unit/modeling/client_ip_address_semantic.spec.ts +71 -0
  44. package/tests/unit/modeling/username_semantic.spec.ts +81 -0
@@ -0,0 +1,196 @@
1
+ import { Jexl } from '@pawel-up/jexl/Jexl.js'
2
+ import type { DomainEntity } from '../../modeling/DomainEntity.js'
3
+ import {
4
+ type AppliedDataSemantic,
5
+ DataSemantics,
6
+ type SemanticCondition,
7
+ SemanticOperation,
8
+ SemanticType,
9
+ } from '../../modeling/Semantics.js'
10
+ import type { TransformFunction } from '@pawel-up/jexl'
11
+
12
+ /**
13
+ * Cache for JEXL instances to avoid recreation overhead
14
+ */
15
+ const jexlCache = new Map<string, Jexl>()
16
+
17
+ /**
18
+ * Get or create a JEXL instance with optional custom transforms
19
+ */
20
+ export function getJexlInstance(transforms?: Record<string, TransformFunction>): Jexl {
21
+ const cacheKey = transforms ? JSON.stringify(Object.keys(transforms).sort()) : 'default'
22
+
23
+ if (!jexlCache.has(cacheKey)) {
24
+ const jexl = new Jexl()
25
+ if (transforms) {
26
+ Object.entries(transforms).forEach(([name, fn]) => {
27
+ jexl.addTransform(name, fn)
28
+ })
29
+ }
30
+ jexlCache.set(cacheKey, jexl)
31
+ }
32
+
33
+ return jexlCache.get(cacheKey) as Jexl
34
+ }
35
+
36
+ /**
37
+ * Context object passed to semantic condition evaluation
38
+ */
39
+ export interface SemanticExecutionContext {
40
+ /**
41
+ * The entity data being processed
42
+ */
43
+ entity: Record<string, unknown>
44
+ /**
45
+ * Current user context (if authenticated)
46
+ */
47
+ user?: {
48
+ id?: string
49
+ authenticated?: boolean
50
+ roles?: string[]
51
+ [key: string]: unknown
52
+ }
53
+ /**
54
+ * The current database operation
55
+ */
56
+ operation: SemanticOperation
57
+ /**
58
+ * Applied semantics with their field mappings
59
+ */
60
+ appliedSemantics: AppliedDataSemantic[]
61
+ /**
62
+ * Configuration for the current semantic being evaluated
63
+ */
64
+ config?: Record<string, unknown>
65
+ }
66
+
67
+ /**
68
+ * Builds a semantics object for JEXL evaluation that maps semantic types to actual field names.
69
+ *
70
+ * The resulting map should be used in JEXL expressions to reference semantic fields.
71
+ *
72
+ * @returns A map where keys are semantic type identifiers and values are the field names.
73
+ *
74
+ * @example
75
+ * const semanticFieldMap = buildSemanticFieldMap(entity);
76
+ * const result = await jexl.eval("semantics.CreatedTimestamp == null", {
77
+ * semantics: semanticFieldMap,
78
+ * ...
79
+ * }
80
+ */
81
+ export function buildSemanticFieldMap(entity: DomainEntity): Record<string, string> {
82
+ const semanticFieldMap: Record<string, string> = {}
83
+
84
+ for (const property of entity.properties) {
85
+ if (!property.info.name) {
86
+ continue // Skip properties without a name
87
+ }
88
+ for (const semantic of property.semantics) {
89
+ // We use the truncated `semantic.id` as the key, e.g. 'Password' so that it is possible to do something like:
90
+ // `entity[semantics.Password] != null && !entity[semantics.Password].startsWith('$')`
91
+ // Where the `semantics` object is the object returned by this function and
92
+ // passed to the JEXL context.
93
+ const semanticKey = semantic.id.replace('Semantic#', '')
94
+ semanticFieldMap[semanticKey] = property.info.name
95
+ }
96
+ }
97
+ return semanticFieldMap
98
+ }
99
+
100
+ /**
101
+ * Evaluates a semantic condition against the execution context
102
+ * In a real implementation, this would use JEXL to evaluate the expression
103
+ *
104
+ * @param condition The semantic condition to evaluate
105
+ * @param context The execution context containing entity data, user info, etc.
106
+ * @param semanticFieldMap A map of semantic field names to their actual field names.
107
+ * Use the `buildSemanticFieldMap()` function to create this map.
108
+ * @param jexl Optional JEXL instance to use for evaluation, defaults to a cached instance.
109
+ * @returns A promise that resolves to true if the condition is met, false otherwise
110
+ */
111
+ export function evaluateSemanticCondition(
112
+ condition: SemanticCondition,
113
+ context: SemanticExecutionContext,
114
+ semanticFieldMap: Record<string, string>,
115
+ jexl: Jexl = getJexlInstance()
116
+ ): Promise<boolean> {
117
+ // Build the evaluation context
118
+ const evalContext = {
119
+ entity: context.entity,
120
+ user: context.user,
121
+ operation: context.operation,
122
+ config: context.config,
123
+ semantics: semanticFieldMap,
124
+ }
125
+ return jexl.eval(condition.expression, evalContext)
126
+ }
127
+
128
+ /**
129
+ * Check if semantic should execute based on all its conditions
130
+ *
131
+ * @param semantic The semantic to check
132
+ * @param context The execution context containing entity data, user info, etc.
133
+ * @param semanticFieldMap A map of semantic field names to their actual field names.
134
+ * Use the `buildSemanticFieldMap()` function to create this map.
135
+ * @return A promise that resolves to true if all conditions are met, false otherwise
136
+ */
137
+ export async function shouldSemanticExecute(
138
+ semantic: AppliedDataSemantic,
139
+ context: SemanticExecutionContext,
140
+ semanticFieldMap: Record<string, string>
141
+ ): Promise<boolean> {
142
+ const definition = DataSemantics[semantic.id]
143
+ const conditions = definition?.runtime?.conditions
144
+
145
+ if (!conditions || conditions.length === 0) {
146
+ return true
147
+ }
148
+
149
+ const results = await Promise.all(
150
+ conditions.map((condition) => evaluateSemanticCondition(condition, context, semanticFieldMap))
151
+ )
152
+ // All conditions must evaluate to true
153
+ return results.every((result) => result === true)
154
+ }
155
+
156
+ /**
157
+ * Helper to get the actual field name for a semantic type
158
+ */
159
+ export function getFieldNameForSemantic(
160
+ semanticType: SemanticType,
161
+ semanticFieldMap: Record<string, string>
162
+ ): string | undefined {
163
+ const semanticKey = semanticType.replace('Semantic#', '')
164
+ return semanticFieldMap[semanticKey]
165
+ }
166
+
167
+ /**
168
+ * Validates that all semantic references in conditions are available
169
+ */
170
+ export function validateSemanticConditions(
171
+ semantic: AppliedDataSemantic,
172
+ semanticFieldMap: Record<string, string>
173
+ ): string[] {
174
+ const definition = DataSemantics[semantic.id]
175
+ const conditions = definition?.runtime?.conditions || []
176
+ const errors: string[] = []
177
+
178
+ conditions.forEach((condition, index) => {
179
+ // Extract semantic references from the condition expression
180
+ // Look for patterns like "semantics.CreatedTimestamp" or "entity[semantics.Password]"
181
+ const semanticMatches = condition.expression.match(/semantics\.(\w+)/g)
182
+
183
+ if (semanticMatches) {
184
+ semanticMatches.forEach((match) => {
185
+ const semanticKey = match.replace('semantics.', '')
186
+ if (!semanticFieldMap[semanticKey]) {
187
+ errors.push(
188
+ `Condition ${index + 1} references semantic "${semanticKey}" but no field with that semantic was found`
189
+ )
190
+ }
191
+ })
192
+ }
193
+ })
194
+
195
+ return errors
196
+ }
@@ -0,0 +1,71 @@
1
+ import { test } from '@japa/runner'
2
+ import {
3
+ SemanticType,
4
+ DataSemantics,
5
+ isPropertySemantic,
6
+ SemanticCategory,
7
+ SemanticScope,
8
+ SemanticTiming,
9
+ SemanticOperation,
10
+ } from '../../../src/modeling/Semantics.js'
11
+
12
+ test.group('ClientIPAddress Semantic', () => {
13
+ test('should exist in SemanticType enum', ({ assert }) => {
14
+ assert.equal(SemanticType.ClientIPAddress, 'Semantic#ClientIPAddress')
15
+ })
16
+
17
+ test('should exist in DataSemantics registry', ({ assert }) => {
18
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
19
+ assert.isDefined(semantic)
20
+ assert.equal(semantic.id, SemanticType.ClientIPAddress)
21
+ })
22
+
23
+ test('should be a property semantic', ({ assert }) => {
24
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
25
+ assert.isTrue(isPropertySemantic(semantic))
26
+ assert.equal(semantic.scope, SemanticScope.Property)
27
+ })
28
+
29
+ test('should have correct display name and description', ({ assert }) => {
30
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
31
+ assert.equal(semantic.displayName, 'Client IP Address')
32
+ assert.equal(semantic.description, 'Automatically populated client IP address')
33
+ })
34
+
35
+ test('should be in Contact category', ({ assert }) => {
36
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
37
+ assert.equal(semantic.category, SemanticCategory.Contact)
38
+ })
39
+
40
+ test('should only apply to string data types', ({ assert }) => {
41
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
42
+ if (isPropertySemantic(semantic)) {
43
+ assert.deepEqual(semantic.applicableDataTypes, ['string'])
44
+ }
45
+ })
46
+
47
+ test('should not have configuration', ({ assert }) => {
48
+ const semantic = DataSemantics[SemanticType.ClientIPAddress]
49
+ assert.isFalse(semantic.hasConfig)
50
+ })
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
+ })
@@ -0,0 +1,81 @@
1
+ import { test } from '@japa/runner'
2
+ import {
3
+ SemanticType,
4
+ DataSemantics,
5
+ isPropertySemantic,
6
+ SemanticCategory,
7
+ SemanticScope,
8
+ SemanticTiming,
9
+ SemanticOperation,
10
+ } from '../../../src/modeling/Semantics.js'
11
+
12
+ test.group('Username Semantic', () => {
13
+ test('should exist in SemanticType enum', ({ assert }) => {
14
+ assert.equal(SemanticType.Username, 'Semantic#Username')
15
+ })
16
+
17
+ test('should exist in DataSemantics registry', ({ assert }) => {
18
+ const semantic = DataSemantics[SemanticType.Username]
19
+ assert.isDefined(semantic)
20
+ assert.equal(semantic.id, SemanticType.Username)
21
+ })
22
+
23
+ test('should be a property semantic', ({ assert }) => {
24
+ const semantic = DataSemantics[SemanticType.Username]
25
+ assert.isTrue(isPropertySemantic(semantic))
26
+ assert.equal(semantic.scope, SemanticScope.Property)
27
+ })
28
+
29
+ test('should have correct display name and description', ({ assert }) => {
30
+ const semantic = DataSemantics[SemanticType.Username]
31
+ assert.equal(semantic.displayName, 'Username')
32
+ assert.equal(semantic.description, 'User authentication identifier')
33
+ })
34
+
35
+ test('should be in Identity category', ({ assert }) => {
36
+ const semantic = DataSemantics[SemanticType.Username]
37
+ assert.equal(semantic.category, SemanticCategory.Identity)
38
+ })
39
+
40
+ test('should only apply to string data types', ({ assert }) => {
41
+ const semantic = DataSemantics[SemanticType.Username]
42
+ if (isPropertySemantic(semantic)) {
43
+ assert.deepEqual(semantic.applicableDataTypes, ['string'])
44
+ }
45
+ })
46
+
47
+ test('should not have configuration', ({ assert }) => {
48
+ const semantic = DataSemantics[SemanticType.Username]
49
+ assert.isFalse(semantic.hasConfig)
50
+ })
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
+ })