@api-client/core 0.19.21 → 0.19.22

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.19.21",
4
+ "version": "0.19.22",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -474,4 +474,41 @@ export class ExposedEntity extends EventTarget {
474
474
  this.actions.push(action)
475
475
  return action
476
476
  }
477
+
478
+ /**
479
+ * Scans for the indexed and search properties in the `DomainEntity`
480
+ * and recreates the `paginationContract` with all indexed/searched fields.
481
+ *
482
+ * Note, this is a destructive action designed as a helper function when creating
483
+ * an exposed entity to fill up default values. Should not be used if the user
484
+ * didn't request that.
485
+ */
486
+ createPaginationContract(): void {
487
+ const entity = this.api.domain?.findEntity(this.entity.key, this.entity.domain)
488
+ if (!entity) {
489
+ throw new Error(`Entity "${this.entity.key}" not found"`)
490
+ }
491
+ if (!this.paginationContract) {
492
+ this.paginationContract = {
493
+ filterableFields: [],
494
+ searchableFields: [],
495
+ sortableFields: [],
496
+ }
497
+ } else {
498
+ this.paginationContract.filterableFields = []
499
+ this.paginationContract.searchableFields = []
500
+ this.paginationContract.sortableFields = []
501
+ }
502
+ for (const prop of entity.properties) {
503
+ // indexed properties allow sorting and filtering in the List action.
504
+ if (prop.index) {
505
+ this.paginationContract.filterableFields.push(prop.key)
506
+ this.paginationContract.sortableFields.push(prop.key)
507
+ }
508
+ // search properties allow full-text search in the Search action.
509
+ if (prop.search) {
510
+ this.paginationContract.searchableFields.push(prop.key)
511
+ }
512
+ }
513
+ }
477
514
  }
@@ -1,5 +1,5 @@
1
1
  import { test } from '@japa/runner'
2
- import { ApiModel, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
2
+ import { ApiModel, DataDomain, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
3
3
  import { AccessRule, RateLimitingConfiguration } from '../../../src/modeling/index.js'
4
4
  import { ExposedEntityKind } from '../../../src/models/kinds.js'
5
5
 
@@ -417,3 +417,146 @@ test.group('ExposedEntity', () => {
417
417
  assert.equal(childRules[3].rate, 20)
418
418
  }).tags(['@modeling', '@exposed-entity', '@rate-limiting'])
419
419
  })
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Helper: build a DataDomain + ApiModel with a single entity already attached.
423
+ // Returns the domain, api model, and the entity key so tests can add properties
424
+ // and then call createPaginationContract() without repeating boilerplate.
425
+ // ---------------------------------------------------------------------------
426
+ function makeFixture() {
427
+ const domain = new DataDomain()
428
+ domain.info.version = '1.0.0'
429
+ const dm = domain.addModel()
430
+ const entity = domain.addEntity(dm.key)
431
+ const model = new ApiModel()
432
+ model.attachDataDomain(domain)
433
+ return { domain, model, entity }
434
+ }
435
+
436
+ test.group('ExposedEntity.createPaginationContract()', () => {
437
+ test('throws when no domain is attached to the API model', ({ assert }) => {
438
+ const model = new ApiModel() // no domain attached
439
+ const ex = new ExposedEntity(model, { entity: { key: 'some-entity' } })
440
+
441
+ assert.throws(() => ex.createPaginationContract(), 'Entity "some-entity" not found')
442
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
443
+
444
+ test('throws when the entity key does not exist in the domain', ({ assert }) => {
445
+ const { model } = makeFixture()
446
+ const ex = new ExposedEntity(model, { entity: { key: 'non-existent' } })
447
+
448
+ assert.throws(() => ex.createPaginationContract(), 'Entity "non-existent" not found')
449
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
450
+
451
+ test('creates a fresh paginationContract when one is not yet set', ({ assert }) => {
452
+ const { model, entity } = makeFixture()
453
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
454
+
455
+ assert.isUndefined(ex.paginationContract)
456
+ ex.createPaginationContract()
457
+
458
+ assert.isDefined(ex.paginationContract)
459
+ assert.deepEqual(ex.paginationContract!.filterableFields, [])
460
+ assert.deepEqual(ex.paginationContract!.sortableFields, [])
461
+ assert.deepEqual(ex.paginationContract!.searchableFields, [])
462
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
463
+
464
+ test('indexed props are added to filterableFields and sortableFields', ({ assert }) => {
465
+ const { domain, model, entity } = makeFixture()
466
+ domain.addProperty(entity.key, { key: 'status', index: true })
467
+ domain.addProperty(entity.key, { key: 'createdAt', index: true })
468
+
469
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
470
+ ex.createPaginationContract()
471
+
472
+ assert.deepEqual(ex.paginationContract!.filterableFields, ['status', 'createdAt'])
473
+ assert.deepEqual(ex.paginationContract!.sortableFields, ['status', 'createdAt'])
474
+ assert.deepEqual(ex.paginationContract!.searchableFields, [])
475
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
476
+
477
+ test('search props are added to searchableFields only', ({ assert }) => {
478
+ const { domain, model, entity } = makeFixture()
479
+ domain.addProperty(entity.key, { key: 'name', search: true })
480
+ domain.addProperty(entity.key, { key: 'description', search: true })
481
+
482
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
483
+ ex.createPaginationContract()
484
+
485
+ assert.deepEqual(ex.paginationContract!.searchableFields, ['name', 'description'])
486
+ assert.deepEqual(ex.paginationContract!.filterableFields, [])
487
+ assert.deepEqual(ex.paginationContract!.sortableFields, [])
488
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
489
+
490
+ test('a prop with both index and search appears in all three lists', ({ assert }) => {
491
+ const { domain, model, entity } = makeFixture()
492
+ domain.addProperty(entity.key, { key: 'email', index: true, search: true })
493
+
494
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
495
+ ex.createPaginationContract()
496
+
497
+ assert.include(ex.paginationContract!.filterableFields, 'email')
498
+ assert.include(ex.paginationContract!.sortableFields, 'email')
499
+ assert.include(ex.paginationContract!.searchableFields, 'email')
500
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
501
+
502
+ test('props without index or search are not added to any list', ({ assert }) => {
503
+ const { domain, model, entity } = makeFixture()
504
+ domain.addProperty(entity.key, { key: 'bio' }) // no index, no search
505
+
506
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
507
+ ex.createPaginationContract()
508
+
509
+ assert.deepEqual(ex.paginationContract!.filterableFields, [])
510
+ assert.deepEqual(ex.paginationContract!.sortableFields, [])
511
+ assert.deepEqual(ex.paginationContract!.searchableFields, [])
512
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
513
+
514
+ test('resets existing contract arrays before repopulating (destructive)', ({ assert }) => {
515
+ const { domain, model, entity } = makeFixture()
516
+ domain.addProperty(entity.key, { key: 'title', index: true, search: true })
517
+
518
+ const ex = new ExposedEntity(model, {
519
+ entity: { key: entity.key },
520
+ paginationContract: {
521
+ filterableFields: ['stale-filter'],
522
+ sortableFields: ['stale-sort'],
523
+ searchableFields: ['stale-search'],
524
+ },
525
+ })
526
+
527
+ // Sanity check: pre-existing stale values are present
528
+ assert.include(ex.paginationContract!.filterableFields, 'stale-filter')
529
+
530
+ ex.createPaginationContract()
531
+
532
+ // Stale values are gone; only the freshly scanned fields remain
533
+ assert.deepEqual(ex.paginationContract!.filterableFields, ['title'])
534
+ assert.deepEqual(ex.paginationContract!.sortableFields, ['title'])
535
+ assert.deepEqual(ex.paginationContract!.searchableFields, ['title'])
536
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
537
+
538
+ test('entity with no properties produces empty contract lists', ({ assert }) => {
539
+ const { model, entity } = makeFixture() // entity has no props
540
+
541
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
542
+ ex.createPaginationContract()
543
+
544
+ assert.deepEqual(ex.paginationContract!.filterableFields, [])
545
+ assert.deepEqual(ex.paginationContract!.sortableFields, [])
546
+ assert.deepEqual(ex.paginationContract!.searchableFields, [])
547
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
548
+
549
+ test('mixed props: only the flagged ones appear in the relevant lists', ({ assert }) => {
550
+ const { domain, model, entity } = makeFixture()
551
+ domain.addProperty(entity.key, { key: 'id', index: true }) // filterable + sortable
552
+ domain.addProperty(entity.key, { key: 'body', search: true }) // searchable
553
+ domain.addProperty(entity.key, { key: 'metadata' }) // neither
554
+
555
+ const ex = new ExposedEntity(model, { entity: { key: entity.key } })
556
+ ex.createPaginationContract()
557
+
558
+ assert.deepEqual(ex.paginationContract!.filterableFields, ['id'])
559
+ assert.deepEqual(ex.paginationContract!.sortableFields, ['id'])
560
+ assert.deepEqual(ex.paginationContract!.searchableFields, ['body'])
561
+ }).tags(['@modeling', '@exposed-entity', '@pagination'])
562
+ })