@api-client/core 0.19.42 → 0.20.1
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/index.d.ts +1 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +4 -0
- package/build/src/index.js.map +1 -1
- package/build/src/modeling/RuntimeApiModel.d.ts +5 -0
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
- package/build/src/modeling/RuntimeApiModel.js +25 -0
- package/build/src/modeling/RuntimeApiModel.js.map +1 -1
- package/build/src/modeling/helpers/runtime.d.ts +5 -0
- package/build/src/modeling/helpers/runtime.d.ts.map +1 -0
- package/build/src/modeling/helpers/runtime.js +12 -0
- package/build/src/modeling/helpers/runtime.js.map +1 -0
- package/build/src/modeling/types.d.ts +3 -4
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/modeling/validation/api_model_rules.js +1 -1
- package/build/src/modeling/validation/api_model_rules.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modeling/RuntimeApiModel.ts +28 -0
- package/src/modeling/helpers/runtime.ts +12 -0
- package/src/modeling/types.ts +3 -4
- package/src/modeling/validation/api_model_rules.ts +1 -1
- package/tests/unit/modeling/RuntimeApiModel.spec.ts +58 -0
- package/tests/unit/modeling/api_model.spec.ts +6 -6
- package/tests/unit/modeling/generators/OasGenerator.spec.ts +1 -1
- package/tests/unit/modeling/helpers/runtime.spec.ts +48 -0
- package/tests/unit/modeling/validation/api_model_rules.spec.ts +2 -2
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import { SemanticType } from './Semantics.js'
|
|
|
8
8
|
import type { DomainEntity } from './DomainEntity.js'
|
|
9
9
|
import type { DomainProperty } from './DomainProperty.js'
|
|
10
10
|
import { AccessRule, AccessRuleExecutionPhase } from './rules/AccessRule.js'
|
|
11
|
+
import { Exception } from '../exceptions/exception.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Identifies a specific exposed entity and its action kind.
|
|
@@ -93,6 +94,19 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
93
94
|
*/
|
|
94
95
|
#actionRulesCache = new WeakMap<Action, ActionRulesCache>()
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Cached session properties for fast runtime lookup.
|
|
99
|
+
*/
|
|
100
|
+
readonly #sessionProperties = new Map<string, DomainProperty>()
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns a readonly map of session properties.
|
|
104
|
+
* Note, it creates a copy of the cached map to prevent modification of the internal state.
|
|
105
|
+
*/
|
|
106
|
+
public get sessionProperties(): ReadonlyMap<string, DomainProperty> {
|
|
107
|
+
return new Map(this.#sessionProperties)
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
constructor(schema: RuntimeApiModelSchema, domainSchema: DataDomainSchema) {
|
|
97
111
|
super(schema, domainSchema)
|
|
98
112
|
|
|
@@ -102,6 +116,20 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
102
116
|
|
|
103
117
|
this.#cacheEntitiesAndProperties()
|
|
104
118
|
this.#precomputeAccessRules()
|
|
119
|
+
this.#precomputeSessionProperties()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#precomputeSessionProperties(): void {
|
|
123
|
+
if (!this.session || !this.domain) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
for (const prop of this.session.properties) {
|
|
127
|
+
const domainProperty = this.domain.findProperty(prop.key, prop.domain)
|
|
128
|
+
if (!domainProperty) {
|
|
129
|
+
throw new Exception(`Session property ${prop.key} not found in domain`)
|
|
130
|
+
}
|
|
131
|
+
this.#sessionProperties.set(prop.key, domainProperty)
|
|
132
|
+
}
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
#precomputeAccessRules(): void {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Only here all NodeJS runtime helpers are allowed
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deterministically truncates long PostgreSQL identifiers to the 63-byte limit.
|
|
7
|
+
*/
|
|
8
|
+
export function truncateIdentifier(name: string): string {
|
|
9
|
+
if (name.length <= 63) return name
|
|
10
|
+
const hash = crypto.createHash('md5').update(name).digest('hex').substring(0, 8)
|
|
11
|
+
return `${name.substring(0, 54)}_${hash}`
|
|
12
|
+
}
|
package/src/modeling/types.ts
CHANGED
|
@@ -356,12 +356,11 @@ export interface SessionConfiguration {
|
|
|
356
356
|
*/
|
|
357
357
|
secret: string
|
|
358
358
|
/**
|
|
359
|
-
* The properties
|
|
359
|
+
* The properties to be set on the session `User` object.
|
|
360
360
|
* These properties become available in the `request.auth` object at runtime.
|
|
361
|
-
*
|
|
362
|
-
* In practice, these are the ids of the properties in the `User` entity.
|
|
361
|
+
* The properties are coming from the `User` entity (marked by the Semantic model).
|
|
363
362
|
*/
|
|
364
|
-
properties:
|
|
363
|
+
properties: AssociationTarget[]
|
|
365
364
|
/**
|
|
366
365
|
* The cookie-based session transport configuration.
|
|
367
366
|
*/
|
|
@@ -231,7 +231,7 @@ export function validateApiModelSecurity(model: ApiModel): ApiModelValidationIte
|
|
|
231
231
|
break
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
if (roleProp && !model.session.properties.
|
|
234
|
+
if (roleProp && !model.session.properties.find((p) => p.key === roleProp?.key)) {
|
|
235
235
|
issues.push({
|
|
236
236
|
code: createCode('API', 'MISSING_RBAC_SESSION_PROPERTY'),
|
|
237
237
|
message: 'The user role must be included in the session data for permissions to work.',
|
|
@@ -153,6 +153,64 @@ test.group('RuntimeApiModel', () => {
|
|
|
153
153
|
assert.equal(runtimeModel.cachedProperties.username?.key, usernameProp.key)
|
|
154
154
|
}).tags(['@modeling', '@runtime'])
|
|
155
155
|
|
|
156
|
+
test('precomputes session properties correctly', ({ assert }) => {
|
|
157
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
158
|
+
const modelNode = domain.addModel({ key: 'users' })
|
|
159
|
+
const userEntity = modelNode.addEntity({ info: { name: 'User' }, semantics: [{ id: SemanticType.User }] })
|
|
160
|
+
const prop1 = userEntity.addProperty({ info: { name: 'Prop1' } })
|
|
161
|
+
const prop2 = userEntity.addProperty({ info: { name: 'Prop2' } })
|
|
162
|
+
|
|
163
|
+
const schema = {
|
|
164
|
+
key: 'api-1',
|
|
165
|
+
info: { name: 'Test API' },
|
|
166
|
+
routingMap: {},
|
|
167
|
+
session: {
|
|
168
|
+
properties: [{ key: prop1.key, domain: domain.key }, { key: prop2.key }],
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
|
|
173
|
+
|
|
174
|
+
assert.equal(runtimeModel.sessionProperties.size, 2)
|
|
175
|
+
assert.equal(runtimeModel.sessionProperties.get(prop1.key)?.key, prop1.key)
|
|
176
|
+
assert.equal(runtimeModel.sessionProperties.get(prop2.key)?.key, prop2.key)
|
|
177
|
+
}).tags(['@modeling', '@runtime'])
|
|
178
|
+
|
|
179
|
+
test('throws exception if session property is missing from domain', ({ assert }) => {
|
|
180
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
181
|
+
const modelNode = domain.addModel({ key: 'users' })
|
|
182
|
+
const userEntity = modelNode.addEntity({ info: { name: 'User' }, semantics: [{ id: SemanticType.User }] })
|
|
183
|
+
userEntity.addProperty({ info: { name: 'Prop1' } })
|
|
184
|
+
|
|
185
|
+
const schema = {
|
|
186
|
+
key: 'api-1',
|
|
187
|
+
info: { name: 'Test API' },
|
|
188
|
+
routingMap: {},
|
|
189
|
+
session: {
|
|
190
|
+
properties: [{ key: 'missing-prop' }],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
assert.throws(
|
|
195
|
+
() => new RuntimeApiModel(schema as any, domain.toJSON()),
|
|
196
|
+
'Session property missing-prop not found in domain'
|
|
197
|
+
)
|
|
198
|
+
}).tags(['@modeling', '@runtime'])
|
|
199
|
+
|
|
200
|
+
test('handles missing session configuration gracefully', ({ assert }) => {
|
|
201
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
202
|
+
|
|
203
|
+
const schema = {
|
|
204
|
+
key: 'api-1',
|
|
205
|
+
info: { name: 'Test API' },
|
|
206
|
+
routingMap: {},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
|
|
210
|
+
|
|
211
|
+
assert.equal(runtimeModel.sessionProperties.size, 0)
|
|
212
|
+
}).tags(['@modeling', '@runtime'])
|
|
213
|
+
|
|
156
214
|
test('evaluateAccess - rejects if any mandatory rule fails', async ({ assert }) => {
|
|
157
215
|
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
158
216
|
const baseModel = new ApiModel(
|
|
@@ -53,7 +53,7 @@ test.group('ApiModel.createSchema()', () => {
|
|
|
53
53
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
54
54
|
authentication: { strategy: 'UsernamePassword' },
|
|
55
55
|
authorization: { strategy: 'RBAC' } as RolesBasedAccessControl,
|
|
56
|
-
session: { secret: 'secret', properties: ['email'] },
|
|
56
|
+
session: { secret: 'secret', properties: [{ key: 'email' }] },
|
|
57
57
|
accessRule: [{ type: 'public' }],
|
|
58
58
|
rateLimiting: { rules: [] },
|
|
59
59
|
termsOfService: 'https://example.com/terms',
|
|
@@ -72,7 +72,7 @@ test.group('ApiModel.createSchema()', () => {
|
|
|
72
72
|
assert.deepEqual(schema.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
73
73
|
assert.deepEqual(schema.authentication, { strategy: 'UsernamePassword' })
|
|
74
74
|
assert.deepEqual(schema.authorization, { strategy: 'RBAC' })
|
|
75
|
-
assert.deepEqual(schema.session, { secret: 'secret', properties: ['email'] })
|
|
75
|
+
assert.deepEqual(schema.session, { secret: 'secret', properties: [{ key: 'email' }] })
|
|
76
76
|
assert.deepEqual(schema.accessRule, [{ type: 'public' }])
|
|
77
77
|
assert.deepEqual(schema.rateLimiting, { rules: [] })
|
|
78
78
|
assert.equal(schema.termsOfService, 'https://example.com/terms')
|
|
@@ -132,7 +132,7 @@ test.group('ApiModel.constructor()', () => {
|
|
|
132
132
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
133
133
|
authentication: { strategy: 'UsernamePassword' },
|
|
134
134
|
authorization: { strategy: 'RBAC' } as RolesBasedAccessControl,
|
|
135
|
-
session: { secret: 'secret', properties: ['email'] },
|
|
135
|
+
session: { secret: 'secret', properties: [{ key: 'email' }] },
|
|
136
136
|
accessRule: [{ type: 'allowPublic' }],
|
|
137
137
|
rateLimiting: { rules: [] },
|
|
138
138
|
termsOfService: 'https://example.com/terms',
|
|
@@ -150,7 +150,7 @@ test.group('ApiModel.constructor()', () => {
|
|
|
150
150
|
assert.deepEqual(model.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
151
151
|
assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
|
|
152
152
|
assert.deepEqual(model.authorization, { strategy: 'RBAC' })
|
|
153
|
-
assert.deepEqual(model.session, { secret: 'secret', properties: ['email'] })
|
|
153
|
+
assert.deepEqual(model.session, { secret: 'secret', properties: [{ key: 'email' }] })
|
|
154
154
|
assert.deepEqual(model.accessRule, [new AllowPublicAccessRule(model)])
|
|
155
155
|
assert.deepEqual(model.rateLimiting, new RateLimitingConfiguration())
|
|
156
156
|
assert.equal(model.termsOfService, 'https://example.com/terms')
|
|
@@ -289,7 +289,7 @@ test.group('ApiModel.toJSON()', () => {
|
|
|
289
289
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
290
290
|
authentication: { strategy: 'UsernamePassword' },
|
|
291
291
|
authorization: { strategy: 'RBAC' } as RolesBasedAccessControl,
|
|
292
|
-
session: { secret: 'secret', properties: ['email'] },
|
|
292
|
+
session: { secret: 'secret', properties: [{ key: 'email' }] },
|
|
293
293
|
accessRule: [{ type: 'allowPublic' }],
|
|
294
294
|
rateLimiting: { rules: [] },
|
|
295
295
|
termsOfService: 'https://example.com/terms',
|
|
@@ -308,7 +308,7 @@ test.group('ApiModel.toJSON()', () => {
|
|
|
308
308
|
assert.deepEqual(json.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
309
309
|
assert.deepEqual(json.authentication, { strategy: 'UsernamePassword' })
|
|
310
310
|
assert.deepEqual(json.authorization, { strategy: 'RBAC' })
|
|
311
|
-
assert.deepEqual(json.session, { secret: 'secret', properties: ['email'] })
|
|
311
|
+
assert.deepEqual(json.session, { secret: 'secret', properties: [{ key: 'email' }] })
|
|
312
312
|
assert.deepEqual(json.accessRule, [{ type: 'allowPublic' }])
|
|
313
313
|
assert.deepEqual(json.rateLimiting, { rules: [] })
|
|
314
314
|
assert.equal(json.termsOfService, 'https://example.com/terms')
|
|
@@ -129,7 +129,7 @@ test.group('OasGenerator', (group) => {
|
|
|
129
129
|
}
|
|
130
130
|
api.accessRule = [new AllowAuthenticatedAccessRule(api)]
|
|
131
131
|
api.session = {
|
|
132
|
-
properties: [email.key],
|
|
132
|
+
properties: [{ key: email.key }],
|
|
133
133
|
secret: 'test-secret',
|
|
134
134
|
cookie: {
|
|
135
135
|
enabled: true,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { truncateIdentifier } from '../../../../src/modeling/helpers/runtime.js'
|
|
3
|
+
|
|
4
|
+
test.group('modeling / helpers / runtime', () => {
|
|
5
|
+
test('returns the original identifier if it is exactly 63 characters long', ({ assert }) => {
|
|
6
|
+
const input = 'a'.repeat(63)
|
|
7
|
+
const result = truncateIdentifier(input)
|
|
8
|
+
assert.equal(result, input)
|
|
9
|
+
assert.equal(result.length, 63)
|
|
10
|
+
}).tags(['@modeling', '@helpers'])
|
|
11
|
+
|
|
12
|
+
test('returns the original identifier if it is less than 63 characters long', ({ assert }) => {
|
|
13
|
+
const input = 'short_identifier'
|
|
14
|
+
const result = truncateIdentifier(input)
|
|
15
|
+
assert.equal(result, input)
|
|
16
|
+
assert.equal(result.length, 16)
|
|
17
|
+
}).tags(['@modeling', '@helpers'])
|
|
18
|
+
|
|
19
|
+
test('truncates identifier to 63 characters if it is longer', ({ assert }) => {
|
|
20
|
+
const input = 'a'.repeat(64)
|
|
21
|
+
const result = truncateIdentifier(input)
|
|
22
|
+
assert.equal(result.length, 63)
|
|
23
|
+
assert.notEqual(result, input)
|
|
24
|
+
}).tags(['@modeling', '@helpers'])
|
|
25
|
+
|
|
26
|
+
test('generates deterministic hash for the same long identifier', ({ assert }) => {
|
|
27
|
+
const input = 'very_long_identifier_that_exceeds_the_limit_of_63_characters_which_is_too_long'
|
|
28
|
+
const result1 = truncateIdentifier(input)
|
|
29
|
+
const result2 = truncateIdentifier(input)
|
|
30
|
+
assert.equal(result1, result2)
|
|
31
|
+
}).tags(['@modeling', '@helpers'])
|
|
32
|
+
|
|
33
|
+
test('generates different hashes for different long identifiers', ({ assert }) => {
|
|
34
|
+
const input1 = 'very_long_identifier_that_exceeds_the_limit_of_63_characters_which_is_too_long_1'
|
|
35
|
+
const input2 = 'very_long_identifier_that_exceeds_the_limit_of_63_characters_which_is_too_long_2'
|
|
36
|
+
const result1 = truncateIdentifier(input1)
|
|
37
|
+
const result2 = truncateIdentifier(input2)
|
|
38
|
+
assert.notEqual(result1, result2)
|
|
39
|
+
}).tags(['@modeling', '@helpers'])
|
|
40
|
+
|
|
41
|
+
test('truncates keeping the first 54 characters', ({ assert }) => {
|
|
42
|
+
const prefix = 'this_is_exactly_54_characters_long_string_prefix_hello'
|
|
43
|
+
const input = `${prefix}_and_some_more_characters_to_exceed_63`
|
|
44
|
+
const result = truncateIdentifier(input)
|
|
45
|
+
assert.isTrue(result.startsWith(`${prefix}_`))
|
|
46
|
+
assert.equal(result.length, 63)
|
|
47
|
+
}).tags(['@modeling', '@helpers'])
|
|
48
|
+
})
|
|
@@ -67,7 +67,7 @@ test.group('ApiModel Validation', () => {
|
|
|
67
67
|
authorization: { strategy: 'RBAC' } as RolesBasedAccessControl,
|
|
68
68
|
session: {
|
|
69
69
|
secret: 'super-secret',
|
|
70
|
-
properties: ['id', 'role'],
|
|
70
|
+
properties: [{ key: 'id' }, { key: 'role' }],
|
|
71
71
|
cookie: {
|
|
72
72
|
enabled: true,
|
|
73
73
|
kind: 'cookie',
|
|
@@ -401,7 +401,7 @@ test.group('ApiModel Validation', () => {
|
|
|
401
401
|
authorization: { strategy: 'RBAC' } as RolesBasedAccessControl,
|
|
402
402
|
session: {
|
|
403
403
|
secret: 'super-secret',
|
|
404
|
-
properties: ['id', 'role'],
|
|
404
|
+
properties: [{ key: 'id' }, { key: 'role' }],
|
|
405
405
|
cookie: {
|
|
406
406
|
enabled: true,
|
|
407
407
|
kind: 'cookie',
|