@api-client/core 0.19.9 → 0.19.10
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/Testing.md +1 -1
- package/build/src/decorators/observed.d.ts.map +1 -1
- package/build/src/decorators/observed.js +91 -0
- package/build/src/decorators/observed.js.map +1 -1
- package/build/src/modeling/ApiModel.d.ts +21 -7
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +70 -29
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/DomainValidation.d.ts +1 -1
- package/build/src/modeling/DomainValidation.d.ts.map +1 -1
- package/build/src/modeling/DomainValidation.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +14 -0
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +59 -6
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/actions/Action.d.ts +11 -1
- package/build/src/modeling/actions/Action.d.ts.map +1 -1
- package/build/src/modeling/actions/Action.js +21 -3
- package/build/src/modeling/actions/Action.js.map +1 -1
- package/build/src/modeling/actions/CreateAction.d.ts +2 -1
- package/build/src/modeling/actions/CreateAction.d.ts.map +1 -1
- package/build/src/modeling/actions/CreateAction.js +2 -2
- package/build/src/modeling/actions/CreateAction.js.map +1 -1
- package/build/src/modeling/actions/DeleteAction.d.ts +2 -1
- package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -1
- package/build/src/modeling/actions/DeleteAction.js +2 -2
- package/build/src/modeling/actions/DeleteAction.js.map +1 -1
- package/build/src/modeling/actions/ListAction.d.ts +2 -1
- package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
- package/build/src/modeling/actions/ListAction.js +2 -2
- package/build/src/modeling/actions/ListAction.js.map +1 -1
- package/build/src/modeling/actions/ReadAction.d.ts +2 -1
- package/build/src/modeling/actions/ReadAction.d.ts.map +1 -1
- package/build/src/modeling/actions/ReadAction.js +2 -2
- package/build/src/modeling/actions/ReadAction.js.map +1 -1
- package/build/src/modeling/actions/SearchAction.d.ts +2 -1
- package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
- package/build/src/modeling/actions/SearchAction.js +2 -2
- package/build/src/modeling/actions/SearchAction.js.map +1 -1
- package/build/src/modeling/actions/UpdateAction.d.ts +2 -1
- package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -1
- package/build/src/modeling/actions/UpdateAction.js +2 -2
- package/build/src/modeling/actions/UpdateAction.js.map +1 -1
- package/build/src/modeling/actions/index.d.ts +2 -1
- package/build/src/modeling/actions/index.d.ts.map +1 -1
- package/build/src/modeling/actions/index.js +7 -7
- package/build/src/modeling/actions/index.js.map +1 -1
- package/build/src/modeling/index.d.ts +1 -0
- package/build/src/modeling/index.d.ts.map +1 -1
- package/build/src/modeling/index.js +1 -0
- package/build/src/modeling/index.js.map +1 -1
- package/build/src/modeling/types.d.ts +67 -0
- 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.d.ts +15 -0
- package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -0
- package/build/src/modeling/validation/api_model_rules.js +599 -0
- package/build/src/modeling/validation/api_model_rules.js.map +1 -0
- package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/association_validation.js +1 -3
- package/build/src/modeling/validation/association_validation.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +8 -8
- package/eslint.config.js +0 -1
- package/package.json +17 -122
- package/src/decorators/observed.ts +91 -0
- package/src/modeling/ApiModel.ts +73 -33
- package/src/modeling/DomainValidation.ts +1 -1
- package/src/modeling/ExposedEntity.ts +63 -9
- package/src/modeling/actions/Action.ts +25 -2
- package/src/modeling/actions/CreateAction.ts +3 -2
- package/src/modeling/actions/DeleteAction.ts +3 -2
- package/src/modeling/actions/ListAction.ts +3 -2
- package/src/modeling/actions/ReadAction.ts +3 -2
- package/src/modeling/actions/SearchAction.ts +3 -2
- package/src/modeling/actions/UpdateAction.ts +3 -2
- package/src/modeling/types.ts +70 -0
- package/src/modeling/validation/api_model_rules.ts +640 -0
- package/src/modeling/validation/api_model_validation_rules.md +58 -0
- package/src/modeling/validation/association_validation.ts +1 -3
- package/tests/unit/modeling/actions/Action.spec.ts +40 -8
- package/tests/unit/modeling/actions/CreateAction.spec.ts +5 -5
- package/tests/unit/modeling/actions/DeleteAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/ListAction.spec.ts +7 -7
- package/tests/unit/modeling/actions/ReadAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/SearchAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/UpdateAction.spec.ts +6 -6
- package/tests/unit/modeling/api_model.spec.ts +190 -13
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +43 -19
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +6 -6
- package/tests/unit/modeling/exposed_entity.spec.ts +123 -3
- package/tests/unit/modeling/exposed_entity_actions.spec.ts +41 -18
- package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +1 -1
- package/tests/unit/modeling/rules/restoring_rules.spec.ts +9 -5
- package/tests/unit/modeling/validation/api_model_rules.spec.ts +324 -0
- package/tsconfig.browser.json +1 -1
- package/tsconfig.node.json +1 -1
- package/bin/test-web.ts +0 -6
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '../../../src/index.js'
|
|
14
14
|
import { AllowPublicAccessRule } from '../../../src/modeling/rules/AllowPublic.js'
|
|
15
15
|
import { RateLimitingConfiguration } from '../../../src/modeling/rules/RateLimitingConfiguration.js'
|
|
16
|
-
import {
|
|
16
|
+
import { ReadActionSchema } from '../../../src/modeling/index.js'
|
|
17
17
|
|
|
18
18
|
test.group('ApiModel.createSchema()', () => {
|
|
19
19
|
test('creates a schema with default values', ({ assert }) => {
|
|
@@ -97,7 +97,7 @@ test.group('ApiModel.constructor()', () => {
|
|
|
97
97
|
assert.typeOf(model.key, 'string')
|
|
98
98
|
assert.isNotEmpty(model.key)
|
|
99
99
|
assert.equal(model.info.name, 'Unnamed API')
|
|
100
|
-
assert.
|
|
100
|
+
assert.equal(model.exposes.size, 0)
|
|
101
101
|
assert.isUndefined(model.user)
|
|
102
102
|
assert.isUndefined(model.authentication)
|
|
103
103
|
assert.isUndefined(model.authorization)
|
|
@@ -140,8 +140,8 @@ test.group('ApiModel.constructor()', () => {
|
|
|
140
140
|
|
|
141
141
|
assert.equal(model.key, 'test-api')
|
|
142
142
|
assert.equal(model.info.name, 'Test API')
|
|
143
|
-
assert.
|
|
144
|
-
assert.equal(model.exposes[0].key, 'entity1', 'exposed entity should have correct key')
|
|
143
|
+
assert.equal(model.exposes.size, 1, 'should have one exposed entity')
|
|
144
|
+
assert.equal(Array.from(model.exposes.values())[0]!.entity.key, 'entity1', 'exposed entity should have correct key')
|
|
145
145
|
assert.deepEqual(model.user, { key: 'user-entity' })
|
|
146
146
|
assert.deepEqual(model.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
147
147
|
assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
|
|
@@ -216,7 +216,7 @@ test.group('ApiModel.constructor()', () => {
|
|
|
216
216
|
notified = true
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
model.exposes[0]
|
|
219
|
+
Array.from(model.exposes.values())[0]!.actions[0]!.kind = 'write'
|
|
220
220
|
await Promise.resolve()
|
|
221
221
|
assert.isTrue(notified)
|
|
222
222
|
}).tags(['@modeling', '@api', '@observed'])
|
|
@@ -290,20 +290,22 @@ test.group('ApiModel.toJSON()', () => {
|
|
|
290
290
|
|
|
291
291
|
test('actions are immutable', ({ assert }) => {
|
|
292
292
|
const model = new ApiModel()
|
|
293
|
-
const action =
|
|
293
|
+
const action: ReadActionSchema = {
|
|
294
|
+
kind: 'read',
|
|
295
|
+
}
|
|
294
296
|
const entityKey = 'get-entity'
|
|
295
297
|
const exposed: ExposedEntitySchema = {
|
|
296
298
|
key: entityKey,
|
|
297
|
-
actions: [action
|
|
299
|
+
actions: [action],
|
|
298
300
|
hasCollection: true,
|
|
299
301
|
kind: ExposedEntityKind,
|
|
300
302
|
resourcePath: '/',
|
|
301
303
|
entity: { key: entityKey },
|
|
302
304
|
}
|
|
303
|
-
model.exposes.
|
|
305
|
+
model.exposes.set(exposed.key, new ExposedEntity(model, exposed))
|
|
304
306
|
const json = model.toJSON()
|
|
305
|
-
json.exposes[0]
|
|
306
|
-
assert.equal(model.exposes[0]
|
|
307
|
+
json.exposes![0]!.actions![0]!.kind = 'write'
|
|
308
|
+
assert.equal(Array.from(model.exposes.values())[0]!.actions[0]!.kind, 'read')
|
|
307
309
|
}).tags(['@modeling', '@api', '@serialization'])
|
|
308
310
|
})
|
|
309
311
|
|
|
@@ -319,15 +321,190 @@ test.group('ApiModel.getExposedEntity()', () => {
|
|
|
319
321
|
resourcePath: '/',
|
|
320
322
|
entity: { key: entityKey },
|
|
321
323
|
}
|
|
322
|
-
model.exposes.
|
|
324
|
+
model.exposes.set(exposed.key, new ExposedEntity(model, exposed))
|
|
323
325
|
|
|
324
|
-
const retrievedEntity = model.
|
|
326
|
+
const retrievedEntity = model.exposes.get(entityKey)
|
|
325
327
|
assert.deepEqual(retrievedEntity?.toJSON(), exposed)
|
|
326
328
|
}).tags(['@modeling', '@api'])
|
|
327
329
|
|
|
328
330
|
test('returns undefined if entity is not exposed', ({ assert }) => {
|
|
329
331
|
const model = new ApiModel()
|
|
330
|
-
const retrievedEntity = model.
|
|
332
|
+
const retrievedEntity = model.exposes.get('non-exposed-entity')
|
|
331
333
|
assert.isUndefined(retrievedEntity)
|
|
332
334
|
}).tags(['@modeling', '@api'])
|
|
333
335
|
})
|
|
336
|
+
|
|
337
|
+
test.group('ApiModel.findResourcePathCollision()', () => {
|
|
338
|
+
test('returns the entity if resource path collides', ({ assert }) => {
|
|
339
|
+
const model = new ApiModel()
|
|
340
|
+
model.exposes.set(
|
|
341
|
+
'ent1',
|
|
342
|
+
new ExposedEntity(model, {
|
|
343
|
+
key: 'ent1',
|
|
344
|
+
kind: ExposedEntityKind,
|
|
345
|
+
resourcePath: '/test',
|
|
346
|
+
isRoot: true,
|
|
347
|
+
hasCollection: false,
|
|
348
|
+
actions: [],
|
|
349
|
+
entity: { key: 'domain1' },
|
|
350
|
+
})
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const collision = model.findResourcePathCollision('/test')
|
|
354
|
+
assert.isDefined(collision)
|
|
355
|
+
assert.equal(collision?.key, 'ent1')
|
|
356
|
+
}).tags(['@modeling', '@api'])
|
|
357
|
+
|
|
358
|
+
test('returns undefined if no collision', ({ assert }) => {
|
|
359
|
+
const model = new ApiModel()
|
|
360
|
+
model.exposes.set(
|
|
361
|
+
'ent1',
|
|
362
|
+
new ExposedEntity(model, {
|
|
363
|
+
key: 'ent1',
|
|
364
|
+
kind: ExposedEntityKind,
|
|
365
|
+
resourcePath: '/test',
|
|
366
|
+
isRoot: true,
|
|
367
|
+
hasCollection: false,
|
|
368
|
+
actions: [],
|
|
369
|
+
entity: { key: 'domain1' },
|
|
370
|
+
})
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
assert.isUndefined(model.findResourcePathCollision('/other'))
|
|
374
|
+
}).tags(['@modeling', '@api'])
|
|
375
|
+
|
|
376
|
+
test('ignores the entity with the ignore key', ({ assert }) => {
|
|
377
|
+
const model = new ApiModel()
|
|
378
|
+
model.exposes.set(
|
|
379
|
+
'ent1',
|
|
380
|
+
new ExposedEntity(model, {
|
|
381
|
+
key: 'ent1',
|
|
382
|
+
kind: ExposedEntityKind,
|
|
383
|
+
resourcePath: '/test',
|
|
384
|
+
isRoot: true,
|
|
385
|
+
hasCollection: false,
|
|
386
|
+
actions: [],
|
|
387
|
+
entity: { key: 'domain1' },
|
|
388
|
+
})
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert.isUndefined(model.findResourcePathCollision('/test', 'ent1'))
|
|
392
|
+
}).tags(['@modeling', '@api'])
|
|
393
|
+
|
|
394
|
+
test('ignores non-root entities', ({ assert }) => {
|
|
395
|
+
const model = new ApiModel()
|
|
396
|
+
model.exposes.set(
|
|
397
|
+
'ent1',
|
|
398
|
+
new ExposedEntity(model, {
|
|
399
|
+
key: 'ent1',
|
|
400
|
+
kind: ExposedEntityKind,
|
|
401
|
+
resourcePath: '/test',
|
|
402
|
+
isRoot: false,
|
|
403
|
+
hasCollection: false,
|
|
404
|
+
actions: [],
|
|
405
|
+
entity: { key: 'domain1' },
|
|
406
|
+
})
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
assert.isUndefined(model.findResourcePathCollision('/test'))
|
|
410
|
+
}).tags(['@modeling', '@api'])
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test.group('ApiModel.findCollectionPathCollision()', () => {
|
|
414
|
+
test('returns the entity if collection path collides', ({ assert }) => {
|
|
415
|
+
const model = new ApiModel()
|
|
416
|
+
model.exposes.set(
|
|
417
|
+
'ent1',
|
|
418
|
+
new ExposedEntity(model, {
|
|
419
|
+
key: 'ent1',
|
|
420
|
+
kind: ExposedEntityKind,
|
|
421
|
+
collectionPath: '/tests',
|
|
422
|
+
resourcePath: '/tests/{id}',
|
|
423
|
+
isRoot: true,
|
|
424
|
+
hasCollection: true,
|
|
425
|
+
actions: [],
|
|
426
|
+
entity: { key: 'domain1' },
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const collision = model.findCollectionPathCollision('/tests')
|
|
431
|
+
assert.isDefined(collision)
|
|
432
|
+
assert.equal(collision?.key, 'ent1')
|
|
433
|
+
}).tags(['@modeling', '@api'])
|
|
434
|
+
|
|
435
|
+
test('returns undefined if no collision', ({ assert }) => {
|
|
436
|
+
const model = new ApiModel()
|
|
437
|
+
model.exposes.set(
|
|
438
|
+
'ent1',
|
|
439
|
+
new ExposedEntity(model, {
|
|
440
|
+
key: 'ent1',
|
|
441
|
+
kind: ExposedEntityKind,
|
|
442
|
+
collectionPath: '/tests',
|
|
443
|
+
resourcePath: '/tests/{id}',
|
|
444
|
+
isRoot: true,
|
|
445
|
+
hasCollection: true,
|
|
446
|
+
actions: [],
|
|
447
|
+
entity: { key: 'domain1' },
|
|
448
|
+
})
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
assert.isUndefined(model.findCollectionPathCollision('/other'))
|
|
452
|
+
}).tags(['@modeling', '@api'])
|
|
453
|
+
|
|
454
|
+
test('ignores the entity with the ignore key', ({ assert }) => {
|
|
455
|
+
const model = new ApiModel()
|
|
456
|
+
model.exposes.set(
|
|
457
|
+
'ent1',
|
|
458
|
+
new ExposedEntity(model, {
|
|
459
|
+
key: 'ent1',
|
|
460
|
+
kind: ExposedEntityKind,
|
|
461
|
+
collectionPath: '/tests',
|
|
462
|
+
resourcePath: '/tests/{id}',
|
|
463
|
+
isRoot: true,
|
|
464
|
+
hasCollection: true,
|
|
465
|
+
actions: [],
|
|
466
|
+
entity: { key: 'domain1' },
|
|
467
|
+
})
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
assert.isUndefined(model.findCollectionPathCollision('/tests', 'ent1'))
|
|
471
|
+
}).tags(['@modeling', '@api'])
|
|
472
|
+
|
|
473
|
+
test('ignores non-root entities', ({ assert }) => {
|
|
474
|
+
const model = new ApiModel()
|
|
475
|
+
model.exposes.set(
|
|
476
|
+
'ent1',
|
|
477
|
+
new ExposedEntity(model, {
|
|
478
|
+
key: 'ent1',
|
|
479
|
+
kind: ExposedEntityKind,
|
|
480
|
+
collectionPath: '/tests',
|
|
481
|
+
resourcePath: '/tests/{id}',
|
|
482
|
+
isRoot: false,
|
|
483
|
+
hasCollection: true,
|
|
484
|
+
actions: [],
|
|
485
|
+
entity: { key: 'domain1' },
|
|
486
|
+
})
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
assert.isUndefined(model.findCollectionPathCollision('/tests'))
|
|
490
|
+
}).tags(['@modeling', '@api'])
|
|
491
|
+
|
|
492
|
+
test('ignores entities without collections', ({ assert }) => {
|
|
493
|
+
const model = new ApiModel()
|
|
494
|
+
model.exposes.set(
|
|
495
|
+
'ent1',
|
|
496
|
+
new ExposedEntity(model, {
|
|
497
|
+
key: 'ent1',
|
|
498
|
+
kind: ExposedEntityKind,
|
|
499
|
+
resourcePath: '/tests',
|
|
500
|
+
collectionPath: '/tests',
|
|
501
|
+
isRoot: true,
|
|
502
|
+
hasCollection: false,
|
|
503
|
+
actions: [],
|
|
504
|
+
entity: { key: 'domain1' },
|
|
505
|
+
})
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
assert.isUndefined(model.findCollectionPathCollision('/tests'))
|
|
509
|
+
}).tags(['@modeling', '@api'])
|
|
510
|
+
})
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
|
-
import { ApiModel, DataDomain } from '../../../src/index.js'
|
|
2
|
+
import { ApiModel, DataDomain, type ExposedEntity } from '../../../src/index.js'
|
|
3
|
+
|
|
4
|
+
function findRootExposedEntity(model: ApiModel, entityKey: string) {
|
|
5
|
+
for (const e of model.exposes.values()) {
|
|
6
|
+
if (e.isRoot && e.entity.key === entityKey) {
|
|
7
|
+
return e
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function findNonRootExposedEntity(model: ApiModel, entityKey: string) {
|
|
14
|
+
for (const e of model.exposes.values()) {
|
|
15
|
+
if (!e.isRoot && e.entity.key === entityKey) {
|
|
16
|
+
return e
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
test.group('ApiModel.exposeEntity()', () => {
|
|
5
23
|
test('exposes a new entity', ({ assert }) => {
|
|
@@ -28,7 +46,7 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
28
46
|
const retrievedExposedEntity = model.exposeEntity({ key: e1.key })
|
|
29
47
|
|
|
30
48
|
assert.deepEqual(retrievedExposedEntity.toJSON(), initialExposedEntity.toJSON())
|
|
31
|
-
assert.
|
|
49
|
+
assert.equal(model.exposes.size, 1)
|
|
32
50
|
}).tags(['@modeling', '@api'])
|
|
33
51
|
|
|
34
52
|
test('notifies change when a new entity is exposed', async ({ assert }) => {
|
|
@@ -77,7 +95,7 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
77
95
|
model.attachDataDomain(domain)
|
|
78
96
|
const exposedA = model.exposeEntity({ key: eA.key }, { followAssociations: true })
|
|
79
97
|
// Find nested exposure for B
|
|
80
|
-
const nestedB = model
|
|
98
|
+
const nestedB = findNonRootExposedEntity(model, eB.key)
|
|
81
99
|
assert.isDefined(nestedB)
|
|
82
100
|
assert.deepEqual(nestedB?.parent?.key, exposedA.key)
|
|
83
101
|
assert.strictEqual(nestedB?.collectionPath, '/entitybs')
|
|
@@ -96,15 +114,21 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
96
114
|
model.attachDataDomain(domain)
|
|
97
115
|
model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 4 })
|
|
98
116
|
|
|
99
|
-
assert.
|
|
117
|
+
assert.equal(model.exposes.size, 2, 'has only 2 exposures')
|
|
100
118
|
// Should expose A (root), B (nested under A), but not infinitely nest
|
|
101
|
-
const exposedA = model
|
|
102
|
-
const nestedB = model
|
|
119
|
+
const exposedA = findRootExposedEntity(model, eA.key)
|
|
120
|
+
const nestedB = findNonRootExposedEntity(model, eB.key)
|
|
103
121
|
// There should be only one nested exposure for B under A
|
|
104
122
|
assert.isDefined(exposedA)
|
|
105
123
|
assert.isDefined(nestedB)
|
|
106
124
|
// There should NOT be a nested exposure for A under B
|
|
107
|
-
|
|
125
|
+
let circularA: ExposedEntity | undefined
|
|
126
|
+
for (const e of model.exposes.values()) {
|
|
127
|
+
if (!e.isRoot && e.entity.key === eA.key && e.parent?.key === nestedB?.key) {
|
|
128
|
+
circularA = e
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
108
132
|
assert.isUndefined(circularA)
|
|
109
133
|
})
|
|
110
134
|
|
|
@@ -119,8 +143,8 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
119
143
|
model.attachDataDomain(domain)
|
|
120
144
|
model.exposeEntity({ key: eA.key }, { followAssociations: true })
|
|
121
145
|
// Should only expose A as root, not as nested
|
|
122
|
-
const exposedA = model
|
|
123
|
-
const nestedA = model
|
|
146
|
+
const exposedA = findRootExposedEntity(model, eA.key)
|
|
147
|
+
const nestedA = findNonRootExposedEntity(model, eA.key)
|
|
124
148
|
assert.isDefined(exposedA)
|
|
125
149
|
assert.isUndefined(nestedA)
|
|
126
150
|
})
|
|
@@ -137,11 +161,11 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
137
161
|
const model = new ApiModel()
|
|
138
162
|
model.attachDataDomain(domain)
|
|
139
163
|
model.exposeEntity({ key: eA.key }, { followAssociations: true })
|
|
140
|
-
const nestedB = model
|
|
141
|
-
const nestedC = model
|
|
164
|
+
const nestedB = findNonRootExposedEntity(model, eB.key)
|
|
165
|
+
const nestedC = findNonRootExposedEntity(model, eC.key)
|
|
142
166
|
assert.isDefined(nestedB)
|
|
143
167
|
assert.isDefined(nestedC)
|
|
144
|
-
assert.deepEqual(nestedB?.parent?.key, model
|
|
168
|
+
assert.deepEqual(nestedB?.parent?.key, findRootExposedEntity(model, eA.key)?.key)
|
|
145
169
|
assert.deepEqual(nestedC?.parent?.key, nestedB?.key)
|
|
146
170
|
})
|
|
147
171
|
|
|
@@ -157,12 +181,12 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
157
181
|
const model = new ApiModel()
|
|
158
182
|
model.attachDataDomain(domain)
|
|
159
183
|
model.exposeEntity({ key: eA.key }, { followAssociations: true })
|
|
160
|
-
const nestedB = model
|
|
161
|
-
const nestedC = model
|
|
184
|
+
const nestedB = findNonRootExposedEntity(model, eB.key)
|
|
185
|
+
const nestedC = findNonRootExposedEntity(model, eC.key)
|
|
162
186
|
assert.isDefined(nestedB)
|
|
163
187
|
assert.isDefined(nestedC)
|
|
164
|
-
assert.deepEqual(nestedB?.parent?.key, model
|
|
165
|
-
assert.deepEqual(nestedC?.parent?.key, model
|
|
188
|
+
assert.deepEqual(nestedB?.parent?.key, findRootExposedEntity(model, eA.key)?.key)
|
|
189
|
+
assert.deepEqual(nestedC?.parent?.key, findRootExposedEntity(model, eA.key)?.key)
|
|
166
190
|
})
|
|
167
191
|
|
|
168
192
|
test('respects maxDepth option (A -> B -> C -> D, maxDepth=2)', ({ assert }) => {
|
|
@@ -179,9 +203,9 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
179
203
|
const model = new ApiModel()
|
|
180
204
|
model.attachDataDomain(domain)
|
|
181
205
|
model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 2 })
|
|
182
|
-
const nestedB = model
|
|
183
|
-
const nestedC = model
|
|
184
|
-
const nestedD = model
|
|
206
|
+
const nestedB = findNonRootExposedEntity(model, eB.key)
|
|
207
|
+
const nestedC = findNonRootExposedEntity(model, eC.key)
|
|
208
|
+
const nestedD = findNonRootExposedEntity(model, eD.key)
|
|
185
209
|
assert.isDefined(nestedB)
|
|
186
210
|
assert.isDefined(nestedC)
|
|
187
211
|
assert.isUndefined(nestedD)
|
|
@@ -10,10 +10,10 @@ test.group('ApiModel.removeEntity()', () => {
|
|
|
10
10
|
const model = new ApiModel()
|
|
11
11
|
model.attachDataDomain(domain)
|
|
12
12
|
const exposure = model.exposeEntity({ key: e1.key })
|
|
13
|
-
assert.
|
|
13
|
+
assert.equal(model.exposes.size, 1)
|
|
14
14
|
|
|
15
15
|
model.removeExposedEntity(exposure.key)
|
|
16
|
-
assert.
|
|
16
|
+
assert.equal(model.exposes.size, 0)
|
|
17
17
|
}).tags(['@modeling', '@api'])
|
|
18
18
|
|
|
19
19
|
test('removes an entity and its nested children', ({ assert }) => {
|
|
@@ -28,13 +28,13 @@ test.group('ApiModel.removeEntity()', () => {
|
|
|
28
28
|
model.attachDataDomain(domain)
|
|
29
29
|
const rootExposure = model.exposeEntity({ key: eA.key }, { followAssociations: true })
|
|
30
30
|
// Ensure nested exposure for B was created
|
|
31
|
-
const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
|
|
31
|
+
const nestedB = Array.from(model.exposes.values()).find((e) => !e.isRoot && e.entity.key === eB.key)
|
|
32
32
|
assert.isDefined(nestedB)
|
|
33
|
-
assert.isAbove(model.exposes.
|
|
33
|
+
assert.isAbove(model.exposes.size, 1)
|
|
34
34
|
|
|
35
35
|
// Remove root exposure for A and expect children to be removed as well
|
|
36
36
|
model.removeExposedEntity(rootExposure.key)
|
|
37
|
-
assert.
|
|
37
|
+
assert.equal(model.exposes.size, 0)
|
|
38
38
|
}).tags(['@modeling', '@api'])
|
|
39
39
|
|
|
40
40
|
test('throws error if entity does not exist', ({ assert }) => {
|
|
@@ -50,7 +50,7 @@ test.group('ApiModel.removeEntity()', () => {
|
|
|
50
50
|
() => model.removeExposedEntity('non-existing-key'),
|
|
51
51
|
'Exposed entity with key "non-existing-key" not found.'
|
|
52
52
|
)
|
|
53
|
-
assert.
|
|
53
|
+
assert.equal(model.exposes.size, 1, 'exposes count should remain unchanged')
|
|
54
54
|
}).tags(['@modeling', '@api'])
|
|
55
55
|
|
|
56
56
|
test('notifies change when an entity is removed', async ({ assert }) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { ApiModel, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
|
|
3
|
+
import { AccessRule, RateLimitingConfiguration } from '../../../src/modeling/index.js'
|
|
3
4
|
import { ExposedEntityKind } from '../../../src/models/kinds.js'
|
|
4
5
|
|
|
5
6
|
test.group('ExposedEntity', () => {
|
|
@@ -84,7 +85,11 @@ test.group('ExposedEntity', () => {
|
|
|
84
85
|
const childEx = new ExposedEntity(model, childSchema)
|
|
85
86
|
const grandEx = new ExposedEntity(model, grandSchema)
|
|
86
87
|
// attach to model as instances
|
|
87
|
-
model.exposes = [
|
|
88
|
+
model.exposes = new Map([
|
|
89
|
+
[rootEx.key, rootEx],
|
|
90
|
+
[childEx.key, childEx],
|
|
91
|
+
[grandEx.key, grandEx],
|
|
92
|
+
])
|
|
88
93
|
|
|
89
94
|
// root
|
|
90
95
|
assert.equal(rootEx.getAbsoluteCollectionPath(), '/users')
|
|
@@ -120,7 +125,7 @@ test.group('ExposedEntity', () => {
|
|
|
120
125
|
notified += 1
|
|
121
126
|
})
|
|
122
127
|
|
|
123
|
-
const ex = model.exposes[0]
|
|
128
|
+
const ex = Array.from(model.exposes.values())[0]
|
|
124
129
|
ex.setCollectionPath('items')
|
|
125
130
|
await Promise.resolve() // allow ApiModel.notifyChange microtask to run
|
|
126
131
|
assert.isAtLeast(notified, 1)
|
|
@@ -147,7 +152,7 @@ test.group('ExposedEntity', () => {
|
|
|
147
152
|
notified += 1
|
|
148
153
|
})
|
|
149
154
|
|
|
150
|
-
const ex = model.exposes[0]
|
|
155
|
+
const ex = Array.from(model.exposes.values())[0]
|
|
151
156
|
ex.setResourcePath('/products/{productId}')
|
|
152
157
|
await Promise.resolve()
|
|
153
158
|
assert.isAtLeast(notified, 1)
|
|
@@ -225,4 +230,119 @@ test.group('ExposedEntity', () => {
|
|
|
225
230
|
assert.lengthOf(ex.accessRule!, 1)
|
|
226
231
|
assert.equal(ex.accessRule![0].type, 'allowPublic')
|
|
227
232
|
}).tags(['@modeling', '@exposed-entity', '@immutability'])
|
|
233
|
+
|
|
234
|
+
test('getAllRules() aggregates rules from entity, parent, and API', ({ assert }) => {
|
|
235
|
+
const model = new ApiModel()
|
|
236
|
+
model.accessRule = [new AccessRule({ type: 'allowPublic' })]
|
|
237
|
+
|
|
238
|
+
const rootEx = new ExposedEntity(model, {
|
|
239
|
+
key: 'root',
|
|
240
|
+
entity: { key: 'user' },
|
|
241
|
+
isRoot: true,
|
|
242
|
+
actions: [],
|
|
243
|
+
accessRule: [{ type: 'matchResourceOwner' }],
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const childEx = new ExposedEntity(model, {
|
|
247
|
+
key: 'child',
|
|
248
|
+
entity: { key: 'post' },
|
|
249
|
+
isRoot: false,
|
|
250
|
+
actions: [],
|
|
251
|
+
parent: { key: 'root', association: { key: 'toPosts' } },
|
|
252
|
+
accessRule: [{ type: 'allowAuthenticated' }],
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
model.exposes = new Map([
|
|
256
|
+
[rootEx.key, rootEx],
|
|
257
|
+
[childEx.key, childEx],
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
const childRules = childEx.getAllRules()
|
|
261
|
+
assert.lengthOf(childRules, 3)
|
|
262
|
+
assert.equal(childRules[0].type, 'allowPublic')
|
|
263
|
+
assert.equal(childRules[1].type, 'allowAuthenticated')
|
|
264
|
+
assert.equal(childRules[2].type, 'matchResourceOwner')
|
|
265
|
+
|
|
266
|
+
const rootRules = rootEx.getAllRules()
|
|
267
|
+
assert.lengthOf(rootRules, 2)
|
|
268
|
+
assert.equal(rootRules[0].type, 'allowPublic')
|
|
269
|
+
assert.equal(rootRules[1].type, 'matchResourceOwner')
|
|
270
|
+
}).tags(['@modeling', '@exposed-entity', '@rules'])
|
|
271
|
+
|
|
272
|
+
test('getAllRateLimiters() aggregates rate limiters from entity, parent, and API', ({ assert }) => {
|
|
273
|
+
const model = new ApiModel()
|
|
274
|
+
model.rateLimiting = new RateLimitingConfiguration({ rules: [{ rate: 10, interval: 'second' }] })
|
|
275
|
+
|
|
276
|
+
const rootEx = new ExposedEntity(model, {
|
|
277
|
+
key: 'root',
|
|
278
|
+
entity: { key: 'user' },
|
|
279
|
+
isRoot: true,
|
|
280
|
+
actions: [],
|
|
281
|
+
rateLimiting: { rules: [{ rate: 20, interval: 'minute' }] },
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const childEx = new ExposedEntity(model, {
|
|
285
|
+
key: 'child',
|
|
286
|
+
entity: { key: 'post' },
|
|
287
|
+
isRoot: false,
|
|
288
|
+
actions: [],
|
|
289
|
+
parent: { key: 'root', association: { key: 'toPosts' } },
|
|
290
|
+
rateLimiting: { rules: [{ rate: 30, interval: 'hour' }] },
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
model.exposes = new Map([
|
|
294
|
+
[rootEx.key, rootEx],
|
|
295
|
+
[childEx.key, childEx],
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
const childLimiters = childEx.getAllRateLimiters()
|
|
299
|
+
assert.lengthOf(childLimiters, 3)
|
|
300
|
+
assert.equal(childLimiters[0].rules[0].rate, 10)
|
|
301
|
+
assert.equal(childLimiters[1].rules[0].rate, 30)
|
|
302
|
+
assert.equal(childLimiters[2].rules[0].rate, 20)
|
|
303
|
+
|
|
304
|
+
const rootLimiters = rootEx.getAllRateLimiters()
|
|
305
|
+
assert.lengthOf(rootLimiters, 2)
|
|
306
|
+
assert.equal(rootLimiters[0].rules[0].rate, 10)
|
|
307
|
+
assert.equal(rootLimiters[1].rules[0].rate, 20)
|
|
308
|
+
}).tags(['@modeling', '@exposed-entity', '@rate-limiting'])
|
|
309
|
+
|
|
310
|
+
test('getAllRateLimiterRules() aggregates rate limiter rules flattened', ({ assert }) => {
|
|
311
|
+
const model = new ApiModel()
|
|
312
|
+
model.rateLimiting = new RateLimitingConfiguration({
|
|
313
|
+
rules: [
|
|
314
|
+
{ rate: 10, interval: 'second' },
|
|
315
|
+
{ rate: 15, interval: 'minute' },
|
|
316
|
+
],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const rootEx = new ExposedEntity(model, {
|
|
320
|
+
key: 'root',
|
|
321
|
+
entity: { key: 'user' },
|
|
322
|
+
isRoot: true,
|
|
323
|
+
actions: [],
|
|
324
|
+
rateLimiting: { rules: [{ rate: 20, interval: 'hour' }] },
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const childEx = new ExposedEntity(model, {
|
|
328
|
+
key: 'child',
|
|
329
|
+
entity: { key: 'post' },
|
|
330
|
+
isRoot: false,
|
|
331
|
+
actions: [],
|
|
332
|
+
parent: { key: 'root', association: { key: 'toPosts' } },
|
|
333
|
+
rateLimiting: { rules: [{ rate: 30, interval: 'day' }] },
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
model.exposes = new Map([
|
|
337
|
+
[rootEx.key, rootEx],
|
|
338
|
+
[childEx.key, childEx],
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
const childRules = childEx.getAllRateLimiterRules()
|
|
342
|
+
assert.lengthOf(childRules, 4)
|
|
343
|
+
assert.equal(childRules[0].rate, 10)
|
|
344
|
+
assert.equal(childRules[1].rate, 15)
|
|
345
|
+
assert.equal(childRules[2].rate, 30)
|
|
346
|
+
assert.equal(childRules[3].rate, 20)
|
|
347
|
+
}).tags(['@modeling', '@exposed-entity', '@rate-limiting'])
|
|
228
348
|
})
|