@expo/entity-database-adapter-knex 0.55.0 → 0.58.0
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/AuthorizationResultBasedKnexEntityLoader.d.ts +278 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
- package/build/src/BaseSQLQueryBuilder.js +87 -0
- package/build/src/BaseSQLQueryBuilder.js.map +1 -0
- package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
- package/build/src/EnforcingKnexEntityLoader.js +166 -0
- package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
- package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
- package/build/src/KnexEntityLoaderFactory.js +39 -0
- package/build/src/KnexEntityLoaderFactory.js.map +1 -0
- package/build/src/PaginationStrategy.d.ts +30 -0
- package/build/src/PaginationStrategy.js +35 -0
- package/build/src/PaginationStrategy.js.map +1 -0
- package/build/src/PostgresEntity.d.ts +25 -0
- package/build/src/PostgresEntity.js +39 -0
- package/build/src/PostgresEntity.js.map +1 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
- package/build/src/PostgresEntityDatabaseAdapter.js +33 -11
- package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
- package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
- package/build/src/ReadonlyPostgresEntity.js +39 -0
- package/build/src/ReadonlyPostgresEntity.js.map +1 -0
- package/build/src/SQLOperator.d.ts +267 -0
- package/build/src/SQLOperator.js +474 -0
- package/build/src/SQLOperator.js.map +1 -0
- package/build/src/index.d.ts +15 -0
- package/build/src/index.js +15 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
- package/build/src/internal/EntityKnexDataManager.js +453 -0
- package/build/src/internal/EntityKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexDataManager.d.ts +3 -0
- package/build/src/internal/getKnexDataManager.js +19 -0
- package/build/src/internal/getKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
- package/build/src/internal/utilityTypes.d.ts +5 -0
- package/build/src/internal/utilityTypes.js +5 -0
- package/build/src/internal/utilityTypes.js.map +1 -0
- package/build/src/internal/weakMaps.d.ts +9 -0
- package/build/src/internal/weakMaps.js +20 -0
- package/build/src/internal/weakMaps.js.map +1 -0
- package/build/src/knexLoader.d.ts +18 -0
- package/build/src/knexLoader.js +31 -0
- package/build/src/knexLoader.js.map +1 -0
- package/package.json +6 -5
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +537 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
- package/src/BaseSQLQueryBuilder.ts +114 -0
- package/src/EnforcingKnexEntityLoader.ts +271 -0
- package/src/KnexEntityLoaderFactory.ts +130 -0
- package/src/PaginationStrategy.ts +32 -0
- package/src/PostgresEntity.ts +118 -0
- package/src/PostgresEntityDatabaseAdapter.ts +81 -24
- package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
- package/src/ReadonlyPostgresEntity.ts +115 -0
- package/src/SQLOperator.ts +630 -0
- package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
- package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
- package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
- package/src/__tests__/EntityFields-test.ts +1 -1
- package/src/__tests__/PostgresEntity-test.ts +172 -0
- package/src/__tests__/ReadonlyEntity-test.ts +32 -0
- package/src/__tests__/SQLOperator-test.ts +871 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
- package/src/__tests__/fixtures/TestEntity.ts +131 -0
- package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
- package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
- package/src/index.ts +15 -0
- package/src/internal/EntityKnexDataManager.ts +832 -0
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
- package/src/internal/__tests__/weakMaps-test.ts +25 -0
- package/src/internal/getKnexDataManager.ts +43 -0
- package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
- package/src/internal/utilityTypes.ts +11 -0
- package/src/internal/weakMaps.ts +19 -0
- package/src/knexLoader.ts +110 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EntityDatabaseAdapterEmptyUpdateResultError,
|
|
3
|
-
OrderByOrdering,
|
|
4
3
|
TransactionIsolationLevel,
|
|
5
4
|
ViewerContext,
|
|
6
5
|
} from '@expo/entity';
|
|
@@ -11,7 +10,14 @@ import { knex, Knex } from 'knex';
|
|
|
11
10
|
import nullthrows from 'nullthrows';
|
|
12
11
|
import { setTimeout } from 'timers/promises';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { PaginationSpecification } from '../AuthorizationResultBasedKnexEntityLoader';
|
|
14
|
+
import { NullsOrdering, OrderByOrdering } from '../BasePostgresEntityDatabaseAdapter';
|
|
15
|
+
import { PaginationStrategy } from '../PaginationStrategy';
|
|
16
|
+
import { entityField, unsafeRaw, sql, SQLFragmentHelpers, SQLFragment } from '../SQLOperator';
|
|
17
|
+
import {
|
|
18
|
+
PostgresTestEntity,
|
|
19
|
+
PostgresTestEntityFields,
|
|
20
|
+
} from '../__testfixtures__/PostgresTestEntity';
|
|
15
21
|
import { PostgresTriggerTestEntity } from '../__testfixtures__/PostgresTriggerTestEntity';
|
|
16
22
|
import { PostgresValidatorTestEntity } from '../__testfixtures__/PostgresValidatorTestEntity';
|
|
17
23
|
import { createKnexIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createKnexIntegrationTestEntityCompanionProvider';
|
|
@@ -123,7 +129,7 @@ describe('postgres entity integration', () => {
|
|
|
123
129
|
const errorToThrow = new Error('Intentional error');
|
|
124
130
|
|
|
125
131
|
await expect(
|
|
126
|
-
vc1.
|
|
132
|
+
vc1.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
127
133
|
'postgres',
|
|
128
134
|
async (queryContext) => {
|
|
129
135
|
// put another in the DB that will be rolled back due to error thrown
|
|
@@ -165,7 +171,7 @@ describe('postgres entity integration', () => {
|
|
|
165
171
|
delay: number,
|
|
166
172
|
): Promise<{ error?: Error }> => {
|
|
167
173
|
try {
|
|
168
|
-
await vc1.
|
|
174
|
+
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
169
175
|
'postgres',
|
|
170
176
|
async (queryContext) => {
|
|
171
177
|
const entity = await PostgresTestEntity.loader(vc1, queryContext).loadByIDAsync(
|
|
@@ -305,6 +311,42 @@ describe('postgres entity integration', () => {
|
|
|
305
311
|
});
|
|
306
312
|
});
|
|
307
313
|
|
|
314
|
+
describe('single field value loading (fetchOneWhereInternalAsync)', () => {
|
|
315
|
+
it('supports one loading', async () => {
|
|
316
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
317
|
+
|
|
318
|
+
await enforceAsyncResult(
|
|
319
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
320
|
+
.setField('name', 'hello')
|
|
321
|
+
.setField('hasACat', false)
|
|
322
|
+
.setField('hasADog', true)
|
|
323
|
+
.createAsync(),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
await enforceAsyncResult(
|
|
327
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
328
|
+
.setField('name', 'world')
|
|
329
|
+
.setField('hasACat', false)
|
|
330
|
+
.setField('hasADog', true)
|
|
331
|
+
.createAsync(),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await enforceAsyncResult(
|
|
335
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
336
|
+
.setField('name', 'wat')
|
|
337
|
+
.setField('hasACat', false)
|
|
338
|
+
.setField('hasADog', false)
|
|
339
|
+
.createAsync(),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const result = await PostgresTestEntity.loaderWithAuthorizationResults(vc1)[
|
|
343
|
+
'loadOneByFieldEqualingAsync'
|
|
344
|
+
]('hasACat', false);
|
|
345
|
+
expect(result?.enforceValue()).not.toBeNull();
|
|
346
|
+
expect(result?.enforceValue().getField('hasACat')).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
308
350
|
it('supports single field and composite field equality loading', async () => {
|
|
309
351
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
310
352
|
|
|
@@ -362,21 +404,21 @@ describe('postgres entity integration', () => {
|
|
|
362
404
|
expect(results2.get(false)).toHaveLength(1);
|
|
363
405
|
});
|
|
364
406
|
|
|
365
|
-
describe('
|
|
366
|
-
it('supports
|
|
407
|
+
describe('SQL operator loading with loadManyBySQL', () => {
|
|
408
|
+
it('supports basic SQL template literal queries', async () => {
|
|
367
409
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
368
410
|
|
|
369
411
|
await enforceAsyncResult(
|
|
370
412
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
371
|
-
.setField('name', '
|
|
372
|
-
.setField('hasACat',
|
|
373
|
-
.setField('hasADog',
|
|
413
|
+
.setField('name', 'Alice')
|
|
414
|
+
.setField('hasACat', true)
|
|
415
|
+
.setField('hasADog', false)
|
|
374
416
|
.createAsync(),
|
|
375
417
|
);
|
|
376
418
|
|
|
377
419
|
await enforceAsyncResult(
|
|
378
420
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
379
|
-
.setField('name', '
|
|
421
|
+
.setField('name', 'Bob')
|
|
380
422
|
.setField('hasACat', false)
|
|
381
423
|
.setField('hasADog', true)
|
|
382
424
|
.createAsync(),
|
|
@@ -384,460 +426,3280 @@ describe('postgres entity integration', () => {
|
|
|
384
426
|
|
|
385
427
|
await enforceAsyncResult(
|
|
386
428
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
387
|
-
.setField('name', '
|
|
388
|
-
.setField('hasACat',
|
|
389
|
-
.setField('hasADog',
|
|
429
|
+
.setField('name', 'Charlie')
|
|
430
|
+
.setField('hasACat', true)
|
|
431
|
+
.setField('hasADog', true)
|
|
390
432
|
.createAsync(),
|
|
391
433
|
);
|
|
392
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
435
|
+
// Test basic SQL query with parameters
|
|
436
|
+
const catOwners = await PostgresTestEntity.knexLoader(vc1)
|
|
437
|
+
.loadManyBySQL(sql`has_a_cat = ${true}`)
|
|
438
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
439
|
+
.executeAsync();
|
|
440
|
+
|
|
441
|
+
expect(catOwners).toHaveLength(2);
|
|
442
|
+
expect(catOwners[0]!.getField('name')).toBe('Alice');
|
|
443
|
+
expect(catOwners[1]!.getField('name')).toBe('Charlie');
|
|
444
|
+
|
|
445
|
+
// Test with limit and offset
|
|
446
|
+
const limitedResults = await PostgresTestEntity.knexLoader(vc1)
|
|
447
|
+
.loadManyBySQL(sql`has_a_cat = ${true}`)
|
|
448
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
449
|
+
.limit(1)
|
|
450
|
+
.offset(1)
|
|
451
|
+
.executeAsync();
|
|
452
|
+
|
|
453
|
+
expect(limitedResults).toHaveLength(1);
|
|
454
|
+
expect(limitedResults[0]!.getField('name')).toBe('Charlie');
|
|
410
455
|
});
|
|
411
456
|
|
|
412
|
-
it('supports
|
|
457
|
+
it('supports SQL helper functions', async () => {
|
|
413
458
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
459
|
+
const { and, or, eq, neq, inArray } = SQLFragmentHelpers;
|
|
414
460
|
|
|
415
461
|
await enforceAsyncResult(
|
|
416
|
-
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
462
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
463
|
+
.setField('name', 'User1')
|
|
464
|
+
.setField('hasACat', true)
|
|
465
|
+
.setField('hasADog', false)
|
|
466
|
+
.createAsync(),
|
|
417
467
|
);
|
|
418
468
|
|
|
419
469
|
await enforceAsyncResult(
|
|
420
|
-
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
470
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
471
|
+
.setField('name', 'User2')
|
|
472
|
+
.setField('hasACat', false)
|
|
473
|
+
.setField('hasADog', true)
|
|
474
|
+
.createAsync(),
|
|
421
475
|
);
|
|
422
476
|
|
|
423
477
|
await enforceAsyncResult(
|
|
424
|
-
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
478
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
479
|
+
.setField('name', 'User3')
|
|
480
|
+
.setField('hasACat', true)
|
|
481
|
+
.setField('hasADog', true)
|
|
482
|
+
.createAsync(),
|
|
425
483
|
);
|
|
426
484
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
expect(
|
|
485
|
+
// Test AND condition
|
|
486
|
+
const bothPets = await PostgresTestEntity.knexLoader(vc1)
|
|
487
|
+
.loadManyBySQL(and(eq('hasACat', true), eq('hasADog', true)))
|
|
488
|
+
.executeAsync();
|
|
489
|
+
|
|
490
|
+
expect(bothPets).toHaveLength(1);
|
|
491
|
+
expect(bothPets[0]!.getField('name')).toBe('User3');
|
|
492
|
+
|
|
493
|
+
// Test OR condition
|
|
494
|
+
const eitherPet = await PostgresTestEntity.knexLoader(vc1)
|
|
495
|
+
.loadManyBySQL(or(eq('hasACat', false), eq('hasADog', false)))
|
|
496
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
497
|
+
.executeAsync();
|
|
498
|
+
|
|
499
|
+
expect(eitherPet).toHaveLength(2);
|
|
500
|
+
expect(eitherPet[0]!.getField('name')).toBe('User1');
|
|
501
|
+
expect(eitherPet[1]!.getField('name')).toBe('User2');
|
|
502
|
+
|
|
503
|
+
// Test IN array
|
|
504
|
+
const specificUsers = await PostgresTestEntity.knexLoader(vc1)
|
|
505
|
+
.loadManyBySQL(inArray('name', ['User1', 'User3']))
|
|
506
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
507
|
+
.executeAsync();
|
|
508
|
+
|
|
509
|
+
expect(specificUsers).toHaveLength(2);
|
|
510
|
+
expect(specificUsers[0]!.getField('name')).toBe('User1');
|
|
511
|
+
expect(specificUsers[1]!.getField('name')).toBe('User3');
|
|
512
|
+
|
|
513
|
+
// Test complex condition
|
|
514
|
+
const complexQuery = await PostgresTestEntity.knexLoader(vc1)
|
|
515
|
+
.loadManyBySQL(and(or(eq('hasACat', true), eq('hasADog', true)), neq('name', 'User2')))
|
|
516
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
517
|
+
.executeAsync();
|
|
518
|
+
|
|
519
|
+
expect(complexQuery).toHaveLength(2);
|
|
520
|
+
expect(complexQuery[0]!.getField('name')).toBe('User1');
|
|
521
|
+
expect(complexQuery[1]!.getField('name')).toBe('User3');
|
|
442
522
|
});
|
|
443
523
|
|
|
444
|
-
it('supports
|
|
524
|
+
it('supports entityField for entity-to-DB field name translation', async () => {
|
|
445
525
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
526
|
+
|
|
446
527
|
await enforceAsyncResult(
|
|
447
528
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
448
|
-
.setField('name', '
|
|
449
|
-
.setField('
|
|
529
|
+
.setField('name', 'EntityFieldUser1')
|
|
530
|
+
.setField('hasACat', true)
|
|
531
|
+
.setField('hasADog', false)
|
|
450
532
|
.createAsync(),
|
|
451
533
|
);
|
|
534
|
+
|
|
452
535
|
await enforceAsyncResult(
|
|
453
536
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
454
|
-
.setField('name', '
|
|
537
|
+
.setField('name', 'EntityFieldUser2')
|
|
538
|
+
.setField('hasACat', false)
|
|
455
539
|
.setField('hasADog', true)
|
|
456
540
|
.createAsync(),
|
|
457
541
|
);
|
|
542
|
+
|
|
458
543
|
await enforceAsyncResult(
|
|
459
544
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
460
|
-
.setField('name',
|
|
545
|
+
.setField('name', 'EntityFieldUser3')
|
|
546
|
+
.setField('hasACat', true)
|
|
461
547
|
.setField('hasADog', true)
|
|
462
548
|
.createAsync(),
|
|
463
549
|
);
|
|
550
|
+
|
|
551
|
+
// Use entityField to reference fields by entity name instead of DB column name
|
|
552
|
+
const catOwners = await PostgresTestEntity.knexLoader(vc1)
|
|
553
|
+
.loadManyBySQL(sql`${entityField('hasACat')} = ${true}`)
|
|
554
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
555
|
+
.executeAsync();
|
|
556
|
+
|
|
557
|
+
expect(catOwners).toHaveLength(2);
|
|
558
|
+
expect(catOwners[0]!.getField('name')).toBe('EntityFieldUser1');
|
|
559
|
+
expect(catOwners[1]!.getField('name')).toBe('EntityFieldUser3');
|
|
560
|
+
|
|
561
|
+
// Combine entityField with other SQL constructs
|
|
562
|
+
const bothPets = await PostgresTestEntity.knexLoader(vc1)
|
|
563
|
+
.loadManyBySQL(
|
|
564
|
+
sql`${entityField('hasACat')} = ${true} AND ${entityField('hasADog')} = ${true}`,
|
|
565
|
+
)
|
|
566
|
+
.executeAsync();
|
|
567
|
+
|
|
568
|
+
expect(bothPets).toHaveLength(1);
|
|
569
|
+
expect(bothPets[0]!.getField('name')).toBe('EntityFieldUser3');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('supports executeFirstAsync', async () => {
|
|
573
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
574
|
+
|
|
575
|
+
await enforceAsyncResult(
|
|
576
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
577
|
+
.setField('name', 'First')
|
|
578
|
+
.setField('hasACat', true)
|
|
579
|
+
.createAsync(),
|
|
580
|
+
);
|
|
581
|
+
|
|
464
582
|
await enforceAsyncResult(
|
|
465
583
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
466
|
-
.setField('name',
|
|
467
|
-
.setField('
|
|
584
|
+
.setField('name', 'Second')
|
|
585
|
+
.setField('hasACat', true)
|
|
468
586
|
.createAsync(),
|
|
469
587
|
);
|
|
470
588
|
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
589
|
+
const firstCatOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1)
|
|
590
|
+
.loadManyBySQL(sql`has_a_cat = ${true}`)
|
|
591
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
592
|
+
.limit(1)
|
|
593
|
+
.executeAsync();
|
|
476
594
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
},
|
|
490
|
-
);
|
|
491
|
-
expect(results2).toHaveLength(2);
|
|
492
|
-
expect(results2.map((e) => e.getField('name'))).toEqual([null, 'a']);
|
|
595
|
+
expect(firstCatOwnerLimit1).toHaveLength(1);
|
|
596
|
+
expect(firstCatOwnerLimit1[0]?.getField('name')).toBe('First');
|
|
597
|
+
|
|
598
|
+
// Test executeFirstAsync with no results
|
|
599
|
+
const noDogOwnerLimit1 = await PostgresTestEntity.knexLoader(vc1)
|
|
600
|
+
.loadManyBySQL(sql`has_a_dog = ${true}`)
|
|
601
|
+
.limit(1)
|
|
602
|
+
.executeAsync();
|
|
603
|
+
|
|
604
|
+
expect(noDogOwnerLimit1).toHaveLength(0);
|
|
493
605
|
});
|
|
494
|
-
});
|
|
495
606
|
|
|
496
|
-
|
|
497
|
-
it('loads by raw where clause', async () => {
|
|
607
|
+
it('supports authorization result-based loading', async () => {
|
|
498
608
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
609
|
+
|
|
499
610
|
await enforceAsyncResult(
|
|
500
611
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
501
|
-
.setField('name', '
|
|
502
|
-
.setField('hasACat',
|
|
503
|
-
.setField('hasADog', true)
|
|
612
|
+
.setField('name', 'AuthTest1')
|
|
613
|
+
.setField('hasACat', true)
|
|
504
614
|
.createAsync(),
|
|
505
615
|
);
|
|
506
616
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
617
|
+
await enforceAsyncResult(
|
|
618
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
619
|
+
.setField('name', 'AuthTest2')
|
|
620
|
+
.setField('hasACat', false)
|
|
621
|
+
.createAsync(),
|
|
510
622
|
);
|
|
511
623
|
|
|
512
|
-
|
|
624
|
+
// Test with authorization results
|
|
625
|
+
const results = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1)
|
|
626
|
+
.loadManyBySQL(sql`name LIKE ${'AuthTest%'}`)
|
|
627
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
628
|
+
.executeAsync();
|
|
629
|
+
|
|
630
|
+
expect(results).toHaveLength(2);
|
|
631
|
+
expect(results[0]!.ok).toBe(true);
|
|
632
|
+
expect(results[1]!.ok).toBe(true);
|
|
633
|
+
|
|
634
|
+
if (results[0]!.ok) {
|
|
635
|
+
expect(results[0]!.value.getField('name')).toBe('AuthTest1');
|
|
636
|
+
}
|
|
637
|
+
if (results[1]!.ok) {
|
|
638
|
+
expect(results[1]!.value.getField('name')).toBe('AuthTest2');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const firstResultLimit1 = await PostgresTestEntity.knexLoaderWithAuthorizationResults(vc1)
|
|
642
|
+
.loadManyBySQL(sql`has_a_cat = ${false}`)
|
|
643
|
+
.limit(1)
|
|
644
|
+
.executeAsync();
|
|
645
|
+
|
|
646
|
+
expect(firstResultLimit1).toHaveLength(1);
|
|
647
|
+
const firstResult = firstResultLimit1[0];
|
|
648
|
+
expect(firstResult?.ok).toBe(true);
|
|
649
|
+
if (firstResult?.ok) {
|
|
650
|
+
expect(firstResult.value.getField('name')).toBe('AuthTest2');
|
|
651
|
+
}
|
|
513
652
|
});
|
|
514
653
|
|
|
515
|
-
it('
|
|
654
|
+
it('supports raw SQL for dynamic queries', async () => {
|
|
516
655
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
656
|
+
|
|
517
657
|
await enforceAsyncResult(
|
|
518
658
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
519
|
-
.setField('name', '
|
|
659
|
+
.setField('name', 'RawTest1')
|
|
660
|
+
.setField('hasACat', true)
|
|
661
|
+
.setField('hasADog', false)
|
|
662
|
+
.createAsync(),
|
|
663
|
+
);
|
|
664
|
+
await enforceAsyncResult(
|
|
665
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
666
|
+
.setField('name', 'RawTest2')
|
|
520
667
|
.setField('hasACat', false)
|
|
521
668
|
.setField('hasADog', true)
|
|
522
669
|
.createAsync(),
|
|
523
670
|
);
|
|
671
|
+
await enforceAsyncResult(
|
|
672
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
673
|
+
.setField('name', 'RawTest3')
|
|
674
|
+
.setField('hasACat', true)
|
|
675
|
+
.setField('hasADog', true)
|
|
676
|
+
.createAsync(),
|
|
677
|
+
);
|
|
524
678
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
679
|
+
// Test raw SQL for dynamic column names with orderBySQL
|
|
680
|
+
const sortColumn = 'name';
|
|
681
|
+
const rawResults = await PostgresTestEntity.knexLoader(vc1)
|
|
682
|
+
.loadManyBySQL(sql`${unsafeRaw('name')} LIKE ${'RawTest%'}`)
|
|
683
|
+
.orderBySQL(sql`${unsafeRaw(sortColumn)}`, OrderByOrdering.DESCENDING)
|
|
684
|
+
.executeAsync();
|
|
685
|
+
|
|
686
|
+
expect(rawResults).toHaveLength(3);
|
|
687
|
+
expect(rawResults[0]!.getField('name')).toBe('RawTest3');
|
|
688
|
+
expect(rawResults[1]!.getField('name')).toBe('RawTest2');
|
|
689
|
+
expect(rawResults[2]!.getField('name')).toBe('RawTest1');
|
|
690
|
+
|
|
691
|
+
// Test complex ORDER BY with CASE statement
|
|
692
|
+
const priorityResults = await PostgresTestEntity.knexLoader(vc1)
|
|
693
|
+
.loadManyBySQL(sql`name LIKE ${'RawTest%'}`)
|
|
694
|
+
.orderBySQL(
|
|
695
|
+
sql`CASE
|
|
696
|
+
WHEN has_a_cat = true AND has_a_dog = true THEN 0
|
|
697
|
+
WHEN has_a_cat = true THEN 1
|
|
698
|
+
ELSE 2
|
|
699
|
+
END`,
|
|
700
|
+
OrderByOrdering.ASCENDING,
|
|
701
|
+
)
|
|
702
|
+
.orderBySQL(sql`${unsafeRaw('name')}`, OrderByOrdering.ASCENDING)
|
|
703
|
+
.executeAsync();
|
|
704
|
+
|
|
705
|
+
expect(priorityResults).toHaveLength(3);
|
|
706
|
+
expect(priorityResults[0]!.getField('name')).toBe('RawTest3'); // has both
|
|
707
|
+
expect(priorityResults[1]!.getField('name')).toBe('RawTest1'); // has cat only
|
|
708
|
+
expect(priorityResults[2]!.getField('name')).toBe('RawTest2'); // has dog only
|
|
709
|
+
|
|
710
|
+
// Test raw SQL with complex expressions - using CASE statement
|
|
711
|
+
const complexExpression = await PostgresTestEntity.knexLoader(vc1)
|
|
712
|
+
.loadManyBySQL(
|
|
713
|
+
sql`${unsafeRaw('CASE WHEN has_a_cat THEN 1 ELSE 0 END')} + ${unsafeRaw(
|
|
714
|
+
'CASE WHEN has_a_dog THEN 1 ELSE 0 END',
|
|
715
|
+
)} >= 1 AND name LIKE ${'RawTest%'}`,
|
|
716
|
+
)
|
|
717
|
+
.executeAsync();
|
|
718
|
+
|
|
719
|
+
expect(complexExpression).toHaveLength(3);
|
|
530
720
|
});
|
|
531
721
|
|
|
532
|
-
it('supports
|
|
722
|
+
it('supports join helper for building complex queries', async () => {
|
|
533
723
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
534
724
|
|
|
535
725
|
await enforceAsyncResult(
|
|
536
726
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
537
|
-
.setField('name', '
|
|
727
|
+
.setField('name', 'JoinTest1')
|
|
728
|
+
.setField('hasACat', true)
|
|
729
|
+
.setField('hasADog', false)
|
|
730
|
+
.createAsync(),
|
|
731
|
+
);
|
|
732
|
+
await enforceAsyncResult(
|
|
733
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
734
|
+
.setField('name', 'JoinTest2')
|
|
735
|
+
.setField('hasACat', true)
|
|
538
736
|
.setField('hasADog', true)
|
|
539
737
|
.createAsync(),
|
|
540
738
|
);
|
|
541
739
|
|
|
740
|
+
// Test join with OR conditions
|
|
741
|
+
const conditions: SQLFragment<PostgresTestEntityFields>[] = [
|
|
742
|
+
sql`name = ${'JoinTest1'}`,
|
|
743
|
+
sql`(has_a_cat = ${true} AND has_a_dog = ${true})`,
|
|
744
|
+
];
|
|
745
|
+
const joinedResults = await PostgresTestEntity.knexLoader(vc1)
|
|
746
|
+
.loadManyBySQL(SQLFragmentHelpers.or<PostgresTestEntityFields>(...conditions))
|
|
747
|
+
.orderBy('name', OrderByOrdering.ASCENDING)
|
|
748
|
+
.executeAsync();
|
|
749
|
+
|
|
750
|
+
expect(joinedResults).toHaveLength(2);
|
|
751
|
+
expect(joinedResults[0]!.getField('name')).toBe('JoinTest1');
|
|
752
|
+
expect(joinedResults[1]!.getField('name')).toBe('JoinTest2');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('provides debug text for SQL queries', async () => {
|
|
756
|
+
// Create a SQL fragment with various types of values
|
|
757
|
+
const fragment = sql`name = ${'TestUser'} AND has_a_cat = ${true} AND age > ${18} AND data = ${{
|
|
758
|
+
key: 'value',
|
|
759
|
+
}} AND created_at > ${new Date('2024-01-01')}`;
|
|
760
|
+
|
|
761
|
+
// Get the debug text
|
|
762
|
+
const debugText = fragment.getDebugString();
|
|
763
|
+
|
|
764
|
+
// Verify the text contains properly formatted values
|
|
765
|
+
expect(debugText).toContain("'TestUser'");
|
|
766
|
+
expect(debugText).toContain('TRUE');
|
|
767
|
+
expect(debugText).toContain('18');
|
|
768
|
+
expect(debugText).toContain('{"key":"value"}');
|
|
769
|
+
expect(debugText).toContain('2024-01-01');
|
|
770
|
+
|
|
771
|
+
// Ensure it's still a valid query (though we wouldn't execute the text directly)
|
|
772
|
+
expect(debugText).toMatch(
|
|
773
|
+
/^name = .* AND has_a_cat = .* AND age > .* AND data = .* AND created_at > .*$/,
|
|
774
|
+
);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('supports orderBySQL for type-safe dynamic ordering', async () => {
|
|
778
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
779
|
+
|
|
780
|
+
// Create test entities with different combinations of fields
|
|
542
781
|
await enforceAsyncResult(
|
|
543
782
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
544
|
-
.setField('name', '
|
|
783
|
+
.setField('name', 'OrderTest1')
|
|
784
|
+
.setField('hasACat', true)
|
|
785
|
+
.setField('hasADog', false)
|
|
786
|
+
.setField('stringArray', ['a', 'b', 'c'])
|
|
787
|
+
.createAsync(),
|
|
788
|
+
);
|
|
789
|
+
await enforceAsyncResult(
|
|
790
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
791
|
+
.setField('name', 'OrderTest2')
|
|
792
|
+
.setField('hasACat', false)
|
|
545
793
|
.setField('hasADog', true)
|
|
794
|
+
.setField('stringArray', ['x', 'y'])
|
|
546
795
|
.createAsync(),
|
|
547
796
|
);
|
|
548
|
-
|
|
549
797
|
await enforceAsyncResult(
|
|
550
798
|
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
551
|
-
.setField('name', '
|
|
799
|
+
.setField('name', 'OrderTest3')
|
|
800
|
+
.setField('hasACat', true)
|
|
552
801
|
.setField('hasADog', true)
|
|
802
|
+
.setField('stringArray', null)
|
|
803
|
+
.createAsync(),
|
|
804
|
+
);
|
|
805
|
+
await enforceAsyncResult(
|
|
806
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
807
|
+
.setField('name', 'OrderTest4')
|
|
808
|
+
.setField('hasACat', false)
|
|
809
|
+
.setField('hasADog', false)
|
|
810
|
+
.setField('stringArray', ['m'])
|
|
553
811
|
.createAsync(),
|
|
554
812
|
);
|
|
555
813
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
{
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
814
|
+
// Test 1: Simple orderBySQL with raw column
|
|
815
|
+
const simpleOrder = await PostgresTestEntity.knexLoader(vc1)
|
|
816
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
817
|
+
.orderBySQL(sql`${unsafeRaw('name')}`, OrderByOrdering.DESCENDING)
|
|
818
|
+
.executeAsync();
|
|
819
|
+
|
|
820
|
+
expect(simpleOrder).toHaveLength(4);
|
|
821
|
+
expect(simpleOrder[0]!.getField('name')).toBe('OrderTest4');
|
|
822
|
+
expect(simpleOrder[1]!.getField('name')).toBe('OrderTest3');
|
|
823
|
+
expect(simpleOrder[2]!.getField('name')).toBe('OrderTest2');
|
|
824
|
+
expect(simpleOrder[3]!.getField('name')).toBe('OrderTest1');
|
|
825
|
+
|
|
826
|
+
// Test 2: Complex CASE statement ordering with parameterized values
|
|
827
|
+
const priority1 = 1;
|
|
828
|
+
const priority2 = 2;
|
|
829
|
+
const priority3 = 3;
|
|
830
|
+
const priority4 = 4;
|
|
831
|
+
const caseOrder = await PostgresTestEntity.knexLoader(vc1)
|
|
832
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
833
|
+
.orderBySQL(
|
|
834
|
+
sql`CASE
|
|
835
|
+
WHEN has_a_cat = true AND has_a_dog = true THEN ${priority1}
|
|
836
|
+
WHEN has_a_cat = true THEN ${priority2}
|
|
837
|
+
WHEN has_a_dog = true THEN ${priority3}
|
|
838
|
+
ELSE ${priority4}
|
|
839
|
+
END`,
|
|
840
|
+
)
|
|
841
|
+
.orderBySQL(sql`${unsafeRaw('name')}`, OrderByOrdering.ASCENDING)
|
|
842
|
+
.executeAsync();
|
|
843
|
+
|
|
844
|
+
expect(caseOrder).toHaveLength(4);
|
|
845
|
+
expect(caseOrder[0]!.getField('name')).toBe('OrderTest3'); // Both pets = 1
|
|
846
|
+
expect(caseOrder[1]!.getField('name')).toBe('OrderTest1'); // Cat only = 2
|
|
847
|
+
expect(caseOrder[2]!.getField('name')).toBe('OrderTest2'); // Dog only = 3
|
|
848
|
+
expect(caseOrder[3]!.getField('name')).toBe('OrderTest4'); // Neither = 4
|
|
849
|
+
|
|
850
|
+
// Test 3: Order by array length (PostgreSQL specific)
|
|
851
|
+
const arrayLengthOrder = await PostgresTestEntity.knexLoader(vc1)
|
|
852
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
853
|
+
.orderBySQL(sql`COALESCE(array_length(string_array, 1), 0)`, OrderByOrdering.DESCENDING)
|
|
854
|
+
.orderBySQL(sql`${unsafeRaw('name')}`, OrderByOrdering.ASCENDING)
|
|
855
|
+
.executeAsync();
|
|
856
|
+
|
|
857
|
+
expect(arrayLengthOrder).toHaveLength(4);
|
|
858
|
+
expect(arrayLengthOrder[0]!.getField('name')).toBe('OrderTest1'); // 3 elements
|
|
859
|
+
expect(arrayLengthOrder[1]!.getField('name')).toBe('OrderTest2'); // 2 elements
|
|
860
|
+
expect(arrayLengthOrder[2]!.getField('name')).toBe('OrderTest4'); // 1 element
|
|
861
|
+
expect(arrayLengthOrder[3]!.getField('name')).toBe('OrderTest3'); // null = 0
|
|
862
|
+
|
|
863
|
+
// Test 4: Combining orderBySQL with limit and offset
|
|
864
|
+
const limitedOrder = await PostgresTestEntity.knexLoader(vc1)
|
|
865
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
866
|
+
.orderBySQL(sql`${unsafeRaw('name')}`, OrderByOrdering.ASCENDING)
|
|
867
|
+
.limit(2)
|
|
868
|
+
.offset(1)
|
|
869
|
+
.executeAsync();
|
|
870
|
+
|
|
871
|
+
expect(limitedOrder).toHaveLength(2);
|
|
872
|
+
expect(limitedOrder[0]!.getField('name')).toBe('OrderTest2');
|
|
873
|
+
expect(limitedOrder[1]!.getField('name')).toBe('OrderTest3');
|
|
874
|
+
|
|
875
|
+
// Test 5: orderBySQL with NULLS FIRST/LAST
|
|
876
|
+
const nullsOrderLast = await PostgresTestEntity.knexLoader(vc1)
|
|
877
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
878
|
+
.orderBySQL(sql`string_array`, OrderByOrdering.ASCENDING, NullsOrdering.LAST)
|
|
879
|
+
.executeAsync();
|
|
880
|
+
|
|
881
|
+
expect(nullsOrderLast).toHaveLength(4);
|
|
882
|
+
expect(nullsOrderLast[0]!.getField('stringArray')).not.toBeNull(); // non-null comes first
|
|
883
|
+
expect(nullsOrderLast[3]!.getField('stringArray')).toBeNull(); // null comes last
|
|
884
|
+
|
|
885
|
+
const nullsOrderFirst = await PostgresTestEntity.knexLoader(vc1)
|
|
886
|
+
.loadManyBySQL(sql`name LIKE ${'OrderTest%'}`)
|
|
887
|
+
.orderBySQL(sql`string_array`, OrderByOrdering.ASCENDING, NullsOrdering.FIRST)
|
|
888
|
+
.executeAsync();
|
|
889
|
+
|
|
890
|
+
expect(nullsOrderFirst).toHaveLength(4);
|
|
891
|
+
expect(nullsOrderFirst[0]!.getField('stringArray')).toBeNull(); // null comes first
|
|
892
|
+
expect(nullsOrderFirst[3]!.getField('stringArray')).not.toBeNull(); // non-null comes last
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('throws error on multiple executeAsync calls', async () => {
|
|
896
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
897
|
+
|
|
898
|
+
// Create a test entity
|
|
899
|
+
await PostgresTestEntity.creator(vc1)
|
|
900
|
+
.setField('name', 'MultiExecTest')
|
|
901
|
+
.setField('hasACat', true)
|
|
902
|
+
.createAsync();
|
|
903
|
+
|
|
904
|
+
// Create a query builder
|
|
905
|
+
const queryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL(
|
|
906
|
+
sql`name = ${'MultiExecTest'}`,
|
|
569
907
|
);
|
|
570
908
|
|
|
571
|
-
|
|
572
|
-
|
|
909
|
+
// First execution should succeed
|
|
910
|
+
const firstResult = await queryBuilder.executeAsync();
|
|
911
|
+
expect(firstResult).toHaveLength(1);
|
|
912
|
+
expect(firstResult[0]!.getField('name')).toBe('MultiExecTest');
|
|
573
913
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
{
|
|
579
|
-
fieldName: 'hasADog',
|
|
580
|
-
order: OrderByOrdering.ASCENDING,
|
|
581
|
-
},
|
|
582
|
-
{
|
|
583
|
-
fieldName: 'name',
|
|
584
|
-
order: OrderByOrdering.DESCENDING,
|
|
585
|
-
},
|
|
586
|
-
],
|
|
587
|
-
});
|
|
914
|
+
// Second execution should throw
|
|
915
|
+
await expect(queryBuilder.executeAsync()).rejects.toThrow(
|
|
916
|
+
'Query has already been executed. Create a new query builder to execute again.',
|
|
917
|
+
);
|
|
588
918
|
|
|
589
|
-
|
|
590
|
-
expect(
|
|
919
|
+
// Third execution should also throw
|
|
920
|
+
await expect(queryBuilder.executeAsync()).rejects.toThrow(
|
|
921
|
+
'Query has already been executed. Create a new query builder to execute again.',
|
|
922
|
+
);
|
|
591
923
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
{
|
|
596
|
-
orderByRaw: 'has_a_dog ASC, name DESC',
|
|
597
|
-
},
|
|
924
|
+
// A new query builder should work fine
|
|
925
|
+
const newQueryBuilder = PostgresTestEntity.knexLoader(vc1).loadManyBySQL(
|
|
926
|
+
sql`name = ${'MultiExecTest'}`,
|
|
598
927
|
);
|
|
599
928
|
|
|
600
|
-
|
|
601
|
-
expect(
|
|
929
|
+
const newResult = await newQueryBuilder.executeAsync();
|
|
930
|
+
expect(newResult).toHaveLength(1);
|
|
931
|
+
expect(newResult[0]!.getField('name')).toBe('MultiExecTest');
|
|
602
932
|
});
|
|
603
933
|
});
|
|
604
934
|
|
|
605
|
-
describe('
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const vc1 = new ViewerContext(
|
|
609
|
-
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
610
|
-
);
|
|
935
|
+
describe('conjunction field equality loading', () => {
|
|
936
|
+
it('supports single fieldValue and multiple fieldValues', async () => {
|
|
937
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
611
938
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
939
|
+
await enforceAsyncResult(
|
|
940
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
941
|
+
.setField('name', 'hello')
|
|
942
|
+
.setField('hasACat', false)
|
|
943
|
+
.setField('hasADog', true)
|
|
944
|
+
.createAsync(),
|
|
945
|
+
);
|
|
618
946
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
947
|
+
await enforceAsyncResult(
|
|
948
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
949
|
+
.setField('name', 'world')
|
|
950
|
+
.setField('hasACat', false)
|
|
951
|
+
.setField('hasADog', true)
|
|
952
|
+
.createAsync(),
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
await enforceAsyncResult(
|
|
956
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
957
|
+
.setField('name', 'wat')
|
|
958
|
+
.setField('hasACat', false)
|
|
959
|
+
.setField('hasADog', false)
|
|
960
|
+
.createAsync(),
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
const results = await PostgresTestEntity.knexLoader(
|
|
964
|
+
vc1,
|
|
965
|
+
).loadManyByFieldEqualityConjunctionAsync([
|
|
966
|
+
{
|
|
967
|
+
fieldName: 'hasACat',
|
|
968
|
+
fieldValue: false,
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
fieldName: 'hasADog',
|
|
972
|
+
fieldValue: true,
|
|
973
|
+
},
|
|
974
|
+
]);
|
|
975
|
+
|
|
976
|
+
expect(results).toHaveLength(2);
|
|
977
|
+
|
|
978
|
+
const results2 = await PostgresTestEntity.knexLoader(
|
|
979
|
+
vc1,
|
|
980
|
+
).loadManyByFieldEqualityConjunctionAsync([
|
|
981
|
+
{ fieldName: 'hasADog', fieldValues: [true, false] },
|
|
982
|
+
]);
|
|
983
|
+
expect(results2).toHaveLength(3);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it('supports query modifiers', async () => {
|
|
987
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
988
|
+
|
|
989
|
+
await enforceAsyncResult(
|
|
990
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'a').createAsync(),
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
await enforceAsyncResult(
|
|
994
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'b').createAsync(),
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
await enforceAsyncResult(
|
|
998
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'c').createAsync(),
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const results = await PostgresTestEntity.knexLoader(
|
|
1002
|
+
vc1,
|
|
1003
|
+
).loadManyByFieldEqualityConjunctionAsync([], {
|
|
1004
|
+
limit: 2,
|
|
1005
|
+
offset: 1,
|
|
1006
|
+
orderBy: [
|
|
1007
|
+
{
|
|
1008
|
+
fieldName: 'name',
|
|
1009
|
+
order: OrderByOrdering.DESCENDING,
|
|
1010
|
+
},
|
|
1011
|
+
],
|
|
1012
|
+
});
|
|
1013
|
+
expect(results).toHaveLength(2);
|
|
1014
|
+
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'a']);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('supports fieldFragment orderBy', async () => {
|
|
1018
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1019
|
+
|
|
1020
|
+
await enforceAsyncResult(
|
|
1021
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1022
|
+
.setField('name', 'alpha')
|
|
1023
|
+
.setField('hasACat', true)
|
|
1024
|
+
.createAsync(),
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
await enforceAsyncResult(
|
|
1028
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1029
|
+
.setField('name', 'beta')
|
|
1030
|
+
.setField('hasACat', false)
|
|
1031
|
+
.createAsync(),
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
await enforceAsyncResult(
|
|
1035
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1036
|
+
.setField('name', 'gamma')
|
|
1037
|
+
.setField('hasACat', true)
|
|
1038
|
+
.createAsync(),
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
// Order by a SQL expression: CASE WHEN has_a_cat THEN 0 ELSE 1 END, name ASC
|
|
1042
|
+
// This should put cat owners first (alpha, gamma), then non-cat owners (beta)
|
|
1043
|
+
const results = await PostgresTestEntity.knexLoader(
|
|
1044
|
+
vc1,
|
|
1045
|
+
).loadManyByFieldEqualityConjunctionAsync([], {
|
|
1046
|
+
orderBy: [
|
|
1047
|
+
{
|
|
1048
|
+
fieldFragment: sql`CASE WHEN has_a_cat = ${true} THEN ${0} ELSE ${1} END`,
|
|
1049
|
+
order: OrderByOrdering.ASCENDING,
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
fieldName: 'name',
|
|
1053
|
+
order: OrderByOrdering.ASCENDING,
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
});
|
|
1057
|
+
expect(results).toHaveLength(3);
|
|
1058
|
+
expect(results.map((e) => e.getField('name'))).toEqual(['alpha', 'gamma', 'beta']);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('rejects fieldFragment containing trailing ASC or DESC', async () => {
|
|
1062
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1063
|
+
|
|
1064
|
+
await expect(
|
|
1065
|
+
PostgresTestEntity.knexLoader(vc1).loadManyByFieldEqualityConjunctionAsync([], {
|
|
1066
|
+
orderBy: [
|
|
1067
|
+
{
|
|
1068
|
+
fieldFragment: sql`${unsafeRaw('name')} ASC`,
|
|
1069
|
+
order: OrderByOrdering.ASCENDING,
|
|
1070
|
+
},
|
|
1071
|
+
],
|
|
1072
|
+
}),
|
|
1073
|
+
).rejects.toThrow('fieldFragment must not contain ASC or DESC at the end');
|
|
1074
|
+
|
|
1075
|
+
await expect(
|
|
1076
|
+
PostgresTestEntity.knexLoader(vc1).loadManyByFieldEqualityConjunctionAsync([], {
|
|
1077
|
+
orderBy: [
|
|
1078
|
+
{
|
|
1079
|
+
fieldFragment: sql`${unsafeRaw('name')} desc`,
|
|
1080
|
+
order: OrderByOrdering.DESCENDING,
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
}),
|
|
1084
|
+
).rejects.toThrow('fieldFragment must not contain ASC or DESC at the end');
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('supports null field values', async () => {
|
|
1088
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1089
|
+
await enforceAsyncResult(
|
|
1090
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1091
|
+
.setField('name', 'a')
|
|
1092
|
+
.setField('hasADog', true)
|
|
1093
|
+
.createAsync(),
|
|
1094
|
+
);
|
|
1095
|
+
await enforceAsyncResult(
|
|
1096
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1097
|
+
.setField('name', 'b')
|
|
1098
|
+
.setField('hasADog', true)
|
|
1099
|
+
.createAsync(),
|
|
1100
|
+
);
|
|
1101
|
+
await enforceAsyncResult(
|
|
1102
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1103
|
+
.setField('name', null)
|
|
1104
|
+
.setField('hasADog', true)
|
|
1105
|
+
.createAsync(),
|
|
1106
|
+
);
|
|
1107
|
+
await enforceAsyncResult(
|
|
1108
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1109
|
+
.setField('name', null)
|
|
1110
|
+
.setField('hasADog', false)
|
|
1111
|
+
.createAsync(),
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
const results = await PostgresTestEntity.knexLoader(
|
|
1115
|
+
vc1,
|
|
1116
|
+
).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]);
|
|
1117
|
+
expect(results).toHaveLength(2);
|
|
1118
|
+
expect(results[0]!.getField('name')).toBeNull();
|
|
1119
|
+
|
|
1120
|
+
const results2 = await PostgresTestEntity.knexLoader(
|
|
1121
|
+
vc1,
|
|
1122
|
+
).loadManyByFieldEqualityConjunctionAsync(
|
|
1123
|
+
[
|
|
1124
|
+
{ fieldName: 'name', fieldValues: ['a', null] },
|
|
1125
|
+
{ fieldName: 'hasADog', fieldValue: true },
|
|
1126
|
+
],
|
|
1127
|
+
{
|
|
1128
|
+
orderBy: [
|
|
1129
|
+
{
|
|
1130
|
+
fieldName: 'name',
|
|
1131
|
+
order: OrderByOrdering.DESCENDING,
|
|
1132
|
+
},
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
);
|
|
1136
|
+
expect(results2).toHaveLength(2);
|
|
1137
|
+
expect(results2.map((e) => e.getField('name'))).toEqual([null, 'a']);
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
describe('raw where clause loading', () => {
|
|
1142
|
+
it('loads by raw where clause', async () => {
|
|
1143
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1144
|
+
await enforceAsyncResult(
|
|
1145
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1146
|
+
.setField('name', 'hello')
|
|
1147
|
+
.setField('hasACat', false)
|
|
1148
|
+
.setField('hasADog', true)
|
|
1149
|
+
.createAsync(),
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync(
|
|
1153
|
+
'name = ?',
|
|
1154
|
+
['hello'],
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
expect(results).toHaveLength(1);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
it('throws with invalid where clause', async () => {
|
|
1161
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1162
|
+
await enforceAsyncResult(
|
|
1163
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1164
|
+
.setField('name', 'hello')
|
|
1165
|
+
.setField('hasACat', false)
|
|
1166
|
+
.setField('hasADog', true)
|
|
1167
|
+
.createAsync(),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
await expect(
|
|
1171
|
+
PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [
|
|
1172
|
+
'hello',
|
|
1173
|
+
]),
|
|
1174
|
+
).rejects.toThrow();
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('supports query modifiers', async () => {
|
|
1178
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1179
|
+
|
|
1180
|
+
await enforceAsyncResult(
|
|
1181
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1182
|
+
.setField('name', 'a')
|
|
1183
|
+
.setField('hasADog', true)
|
|
1184
|
+
.createAsync(),
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
await enforceAsyncResult(
|
|
1188
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1189
|
+
.setField('name', 'b')
|
|
1190
|
+
.setField('hasADog', true)
|
|
1191
|
+
.createAsync(),
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
await enforceAsyncResult(
|
|
1195
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1196
|
+
.setField('name', 'c')
|
|
1197
|
+
.setField('hasADog', true)
|
|
1198
|
+
.createAsync(),
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
const results = await PostgresTestEntity.knexLoader(vc1).loadManyByRawWhereClauseAsync(
|
|
1202
|
+
'has_a_dog = ?',
|
|
1203
|
+
[true],
|
|
1204
|
+
{
|
|
1205
|
+
limit: 2,
|
|
1206
|
+
offset: 1,
|
|
1207
|
+
orderBy: [
|
|
1208
|
+
{
|
|
1209
|
+
fieldName: 'name',
|
|
1210
|
+
order: OrderByOrdering.ASCENDING,
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
},
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
expect(results).toHaveLength(2);
|
|
1217
|
+
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']);
|
|
1218
|
+
|
|
1219
|
+
const resultsMultipleOrderBy = await PostgresTestEntity.knexLoader(
|
|
1220
|
+
vc1,
|
|
1221
|
+
).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
|
|
1222
|
+
orderBy: [
|
|
1223
|
+
{
|
|
1224
|
+
fieldName: 'hasADog',
|
|
1225
|
+
order: OrderByOrdering.ASCENDING,
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
fieldName: 'name',
|
|
1229
|
+
order: OrderByOrdering.DESCENDING,
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
expect(resultsMultipleOrderBy).toHaveLength(3);
|
|
1235
|
+
expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']);
|
|
1236
|
+
|
|
1237
|
+
const resultsOrderByRaw = await PostgresTestEntity.knexLoader(
|
|
1238
|
+
vc1,
|
|
1239
|
+
).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
|
|
1240
|
+
orderBy: [
|
|
1241
|
+
{
|
|
1242
|
+
fieldFragment: sql`${entityField('hasADog')}`,
|
|
1243
|
+
order: OrderByOrdering.ASCENDING,
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
fieldFragment: sql`${entityField('name')}`,
|
|
1247
|
+
order: OrderByOrdering.DESCENDING,
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
expect(resultsOrderByRaw).toHaveLength(3);
|
|
1253
|
+
expect(resultsOrderByRaw.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']);
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
describe('trigger transaction behavior', () => {
|
|
1258
|
+
describe('create', () => {
|
|
1259
|
+
it('rolls back transaction when trigger throws except afterCommit', async () => {
|
|
1260
|
+
const vc1 = new ViewerContext(
|
|
1261
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
await expect(
|
|
1265
|
+
PostgresTriggerTestEntity.creator(vc1).setField('name', 'beforeCreate').createAsync(),
|
|
1266
|
+
).rejects.toThrow('name cannot have value beforeCreate');
|
|
1267
|
+
await expect(
|
|
1268
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeCreate'),
|
|
1269
|
+
).resolves.toBeNull();
|
|
1270
|
+
|
|
1271
|
+
await expect(
|
|
1272
|
+
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterCreate').createAsync(),
|
|
1273
|
+
).rejects.toThrow('name cannot have value afterCreate');
|
|
1274
|
+
await expect(
|
|
1275
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCreate'),
|
|
1276
|
+
).resolves.toBeNull();
|
|
1277
|
+
|
|
1278
|
+
await expect(
|
|
1279
|
+
PostgresTriggerTestEntity.creator(vc1).setField('name', 'beforeAll').createAsync(),
|
|
1280
|
+
).rejects.toThrow('name cannot have value beforeAll');
|
|
1281
|
+
await expect(
|
|
1282
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll'),
|
|
1283
|
+
).resolves.toBeNull();
|
|
1284
|
+
|
|
1285
|
+
await expect(
|
|
1286
|
+
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterAll').createAsync(),
|
|
1287
|
+
).rejects.toThrow('name cannot have value afterAll');
|
|
1288
|
+
await expect(
|
|
1289
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll'),
|
|
1290
|
+
).resolves.toBeNull();
|
|
1291
|
+
|
|
1292
|
+
await expect(
|
|
1293
|
+
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterCommit').createAsync(),
|
|
1294
|
+
).rejects.toThrow('name cannot have value afterCommit');
|
|
1295
|
+
await expect(
|
|
1296
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit'),
|
|
1297
|
+
).resolves.not.toBeNull();
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
describe('update', () => {
|
|
1302
|
+
it('rolls back transaction when trigger throws except afterCommit', async () => {
|
|
1303
|
+
const vc1 = new ViewerContext(
|
|
1304
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1305
|
+
);
|
|
1306
|
+
|
|
1307
|
+
const entity = await PostgresTriggerTestEntity.creator(vc1)
|
|
1308
|
+
.setField('name', 'blah')
|
|
1309
|
+
.createAsync();
|
|
1310
|
+
|
|
1311
|
+
await expect(
|
|
1312
|
+
PostgresTriggerTestEntity.updater(entity).setField('name', 'beforeUpdate').updateAsync(),
|
|
1313
|
+
).rejects.toThrow('name cannot have value beforeUpdate');
|
|
1314
|
+
await expect(
|
|
1315
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeUpdate'),
|
|
1316
|
+
).resolves.toBeNull();
|
|
1317
|
+
|
|
1318
|
+
await expect(
|
|
1319
|
+
PostgresTriggerTestEntity.updater(entity).setField('name', 'afterUpdate').updateAsync(),
|
|
1320
|
+
).rejects.toThrow('name cannot have value afterUpdate');
|
|
1321
|
+
await expect(
|
|
1322
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterUpdate'),
|
|
1323
|
+
).resolves.toBeNull();
|
|
1324
|
+
|
|
1325
|
+
await expect(
|
|
1326
|
+
PostgresTriggerTestEntity.updater(entity).setField('name', 'beforeAll').updateAsync(),
|
|
1327
|
+
).rejects.toThrow('name cannot have value beforeAll');
|
|
1328
|
+
await expect(
|
|
1329
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll'),
|
|
1330
|
+
).resolves.toBeNull();
|
|
1331
|
+
|
|
1332
|
+
await expect(
|
|
1333
|
+
PostgresTriggerTestEntity.updater(entity).setField('name', 'afterAll').updateAsync(),
|
|
1334
|
+
).rejects.toThrow('name cannot have value afterAll');
|
|
1335
|
+
await expect(
|
|
1336
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll'),
|
|
1337
|
+
).resolves.toBeNull();
|
|
1338
|
+
|
|
1339
|
+
await expect(
|
|
1340
|
+
PostgresTriggerTestEntity.updater(entity).setField('name', 'afterCommit').updateAsync(),
|
|
1341
|
+
).rejects.toThrow('name cannot have value afterCommit');
|
|
1342
|
+
await expect(
|
|
1343
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit'),
|
|
1344
|
+
).resolves.not.toBeNull();
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
describe('delete', () => {
|
|
1349
|
+
it('rolls back transaction when trigger throws except afterCommit', async () => {
|
|
1350
|
+
const vc1 = new ViewerContext(
|
|
1351
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
const entityBeforeDelete = await PostgresTriggerTestEntity.creator(vc1)
|
|
1355
|
+
.setField('name', 'beforeDelete')
|
|
1356
|
+
.createAsync();
|
|
1357
|
+
await expect(
|
|
1358
|
+
PostgresTriggerTestEntity.deleter(entityBeforeDelete).deleteAsync(),
|
|
1359
|
+
).rejects.toThrow('name cannot have value beforeDelete');
|
|
1360
|
+
await expect(
|
|
1361
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeDelete'),
|
|
1362
|
+
).resolves.not.toBeNull();
|
|
1363
|
+
|
|
1364
|
+
const entityAfterDelete = await PostgresTriggerTestEntity.creator(vc1)
|
|
1365
|
+
.setField('name', 'afterDelete')
|
|
1366
|
+
.createAsync();
|
|
1367
|
+
await expect(
|
|
1368
|
+
PostgresTriggerTestEntity.deleter(entityAfterDelete).deleteAsync(),
|
|
1369
|
+
).rejects.toThrow('name cannot have value afterDelete');
|
|
1370
|
+
await expect(
|
|
1371
|
+
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterDelete'),
|
|
1372
|
+
).resolves.not.toBeNull();
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
describe('validation transaction behavior', () => {
|
|
1376
|
+
describe('create', () => {
|
|
1377
|
+
it('rolls back transaction when trigger throws ', async () => {
|
|
1378
|
+
const vc1 = new ViewerContext(
|
|
1379
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1382
|
+
await expect(
|
|
1383
|
+
PostgresValidatorTestEntity.creator(vc1)
|
|
1384
|
+
.setField('name', 'beforeCreateAndUpdate')
|
|
1385
|
+
.createAsync(),
|
|
1386
|
+
).rejects.toThrow('name cannot have value beforeCreateAndUpdate');
|
|
1387
|
+
await expect(
|
|
1388
|
+
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
|
1389
|
+
'name',
|
|
1390
|
+
'beforeCreateAndUpdate',
|
|
1391
|
+
),
|
|
1392
|
+
).resolves.toBeNull();
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
1395
|
+
describe('update', () => {
|
|
1396
|
+
it('rolls back transaction when trigger throws ', async () => {
|
|
1397
|
+
const vc1 = new ViewerContext(
|
|
1398
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
const entity = await PostgresValidatorTestEntity.creator(vc1)
|
|
1402
|
+
.setField('name', 'blah')
|
|
1403
|
+
.createAsync();
|
|
1404
|
+
|
|
1405
|
+
await expect(
|
|
1406
|
+
PostgresValidatorTestEntity.updater(entity)
|
|
1407
|
+
.setField('name', 'beforeCreateAndUpdate')
|
|
1408
|
+
.updateAsync(),
|
|
1409
|
+
).rejects.toThrow('name cannot have value beforeCreateAndUpdate');
|
|
1410
|
+
await expect(
|
|
1411
|
+
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
|
1412
|
+
'name',
|
|
1413
|
+
'beforeCreateAndUpdate',
|
|
1414
|
+
),
|
|
1415
|
+
).resolves.toBeNull();
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
describe('delete', () => {
|
|
1419
|
+
it('validation should not run on a delete mutation', async () => {
|
|
1420
|
+
const vc1 = new ViewerContext(
|
|
1421
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
const entityToDelete = await PostgresValidatorTestEntity.creator(vc1)
|
|
1425
|
+
.setField('name', 'shouldBeDeleted')
|
|
1426
|
+
.createAsync();
|
|
1427
|
+
await PostgresValidatorTestEntity.deleter(entityToDelete).deleteAsync();
|
|
1428
|
+
await expect(
|
|
1429
|
+
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
|
1430
|
+
'name',
|
|
1431
|
+
'shouldBeDeleted',
|
|
1432
|
+
),
|
|
1433
|
+
).resolves.toBeNull();
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
describe('queryContext callback behavior', () => {
|
|
1440
|
+
it('calls callbacks correctly', async () => {
|
|
1441
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1442
|
+
|
|
1443
|
+
let preCommitCallCount = 0;
|
|
1444
|
+
let preCommitInnerCallCount = 0;
|
|
1445
|
+
let postCommitCallCount = 0;
|
|
1446
|
+
|
|
1447
|
+
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
|
|
1448
|
+
queryContext.appendPostCommitCallback(async () => {
|
|
1449
|
+
postCommitCallCount++;
|
|
1450
|
+
});
|
|
1451
|
+
queryContext.appendPreCommitCallback(async () => {
|
|
1452
|
+
preCommitCallCount++;
|
|
1453
|
+
}, 0);
|
|
1454
|
+
|
|
1455
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
1456
|
+
innerQueryContext.appendPostCommitCallback(async () => {
|
|
1457
|
+
postCommitCallCount++;
|
|
1458
|
+
});
|
|
1459
|
+
innerQueryContext.appendPreCommitCallback(async () => {
|
|
1460
|
+
preCommitInnerCallCount++;
|
|
1461
|
+
}, 0);
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// this one throws so its post commit shouldn't execute
|
|
1465
|
+
try {
|
|
1466
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
1467
|
+
innerQueryContext.appendPostCommitCallback(async () => {
|
|
1468
|
+
postCommitCallCount++;
|
|
1469
|
+
});
|
|
1470
|
+
innerQueryContext.appendPreCommitCallback(async () => {
|
|
1471
|
+
preCommitInnerCallCount++;
|
|
1472
|
+
throw Error('wat');
|
|
1473
|
+
}, 0);
|
|
1474
|
+
});
|
|
1475
|
+
} catch {}
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
await expect(
|
|
1479
|
+
vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
|
|
1480
|
+
queryContext.appendPostCommitCallback(async () => {
|
|
1481
|
+
postCommitCallCount++;
|
|
1482
|
+
});
|
|
1483
|
+
queryContext.appendPreCommitCallback(async () => {
|
|
1484
|
+
preCommitCallCount++;
|
|
1485
|
+
throw Error('wat');
|
|
1486
|
+
}, 0);
|
|
1487
|
+
}),
|
|
1488
|
+
).rejects.toThrow('wat');
|
|
1489
|
+
|
|
1490
|
+
expect(preCommitCallCount).toBe(2);
|
|
1491
|
+
expect(preCommitInnerCallCount).toBe(2);
|
|
1492
|
+
expect(postCommitCallCount).toBe(2);
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
describe('pagination with loadPageAsync', () => {
|
|
1497
|
+
describe(PaginationStrategy.STANDARD, () => {
|
|
1498
|
+
describe('with standard test data', () => {
|
|
1499
|
+
beforeEach(async () => {
|
|
1500
|
+
const vc = new ViewerContext(
|
|
1501
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1502
|
+
);
|
|
1503
|
+
|
|
1504
|
+
// Create test data with predictable values
|
|
1505
|
+
const names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Henry'];
|
|
1506
|
+
for (let i = 0; i < names.length; i++) {
|
|
1507
|
+
await PostgresTestEntity.creator(vc)
|
|
1508
|
+
.setField('name', names[i]!)
|
|
1509
|
+
.setField('hasACat', i % 2 === 0)
|
|
1510
|
+
.setField('hasADog', i % 3 === 0)
|
|
1511
|
+
.setField('dateField', new Date(2024, 0, i + 1))
|
|
1512
|
+
.createAsync();
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
it('performs forward pagination with first/after', async () => {
|
|
1517
|
+
const vc = new ViewerContext(
|
|
1518
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1519
|
+
);
|
|
1520
|
+
|
|
1521
|
+
// Get first page
|
|
1522
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1523
|
+
first: 3,
|
|
1524
|
+
pagination: {
|
|
1525
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1526
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
1531
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Alice');
|
|
1532
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBe('Bob');
|
|
1533
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Charlie');
|
|
1534
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
1535
|
+
expect(firstPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1536
|
+
|
|
1537
|
+
// Get second page using cursor
|
|
1538
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1539
|
+
first: 3,
|
|
1540
|
+
after: firstPage.pageInfo.endCursor!,
|
|
1541
|
+
pagination: {
|
|
1542
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1543
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1544
|
+
},
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
expect(secondPage.edges).toHaveLength(3);
|
|
1548
|
+
expect(secondPage.edges[0]?.node.getField('name')).toBe('David');
|
|
1549
|
+
expect(secondPage.edges[1]?.node.getField('name')).toBe('Eve');
|
|
1550
|
+
expect(secondPage.edges[2]?.node.getField('name')).toBe('Frank');
|
|
1551
|
+
expect(secondPage.pageInfo.hasNextPage).toBe(true);
|
|
1552
|
+
expect(secondPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
it('getPaginationCursorForEntity produces cursor usable with loadPageAsync', async () => {
|
|
1556
|
+
const vc = new ViewerContext(
|
|
1557
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
// Load first page to get the third entity (Charlie)
|
|
1561
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1562
|
+
first: 3,
|
|
1563
|
+
pagination: {
|
|
1564
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1565
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
const charlieEntity = firstPage.edges[2]!.node;
|
|
1570
|
+
const cursorFromPage = firstPage.edges[2]!.cursor;
|
|
1571
|
+
|
|
1572
|
+
// Get cursor using getPaginationCursorForEntity
|
|
1573
|
+
const cursorFromMethod =
|
|
1574
|
+
PostgresTestEntity.knexLoader(vc).getPaginationCursorForEntity(charlieEntity);
|
|
1575
|
+
|
|
1576
|
+
// cursors should be equal for both loaders
|
|
1577
|
+
expect(cursorFromMethod).toEqual(
|
|
1578
|
+
PostgresTestEntity.knexLoaderWithAuthorizationResults(vc).getPaginationCursorForEntity(
|
|
1579
|
+
charlieEntity,
|
|
1580
|
+
),
|
|
1581
|
+
);
|
|
1582
|
+
|
|
1583
|
+
expect(cursorFromMethod).toBe(cursorFromPage);
|
|
1584
|
+
|
|
1585
|
+
// Use the cursor from getPaginationCursorForEntity to paginate
|
|
1586
|
+
const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1587
|
+
first: 3,
|
|
1588
|
+
after: cursorFromMethod,
|
|
1589
|
+
pagination: {
|
|
1590
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1591
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
expect(nextPage.edges).toHaveLength(3);
|
|
1596
|
+
expect(nextPage.edges[0]?.node.getField('name')).toBe('David');
|
|
1597
|
+
expect(nextPage.edges[1]?.node.getField('name')).toBe('Eve');
|
|
1598
|
+
expect(nextPage.edges[2]?.node.getField('name')).toBe('Frank');
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
it('performs backward pagination with last/before', async () => {
|
|
1602
|
+
const vc = new ViewerContext(
|
|
1603
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
// Get last page
|
|
1607
|
+
const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1608
|
+
last: 3,
|
|
1609
|
+
pagination: {
|
|
1610
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1611
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1612
|
+
},
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
expect(lastPage.edges).toHaveLength(3);
|
|
1616
|
+
expect(lastPage.edges[0]?.node.getField('name')).toBe('Frank');
|
|
1617
|
+
expect(lastPage.edges[1]?.node.getField('name')).toBe('Grace');
|
|
1618
|
+
expect(lastPage.edges[2]?.node.getField('name')).toBe('Henry');
|
|
1619
|
+
expect(lastPage.pageInfo.hasNextPage).toBe(false);
|
|
1620
|
+
expect(lastPage.pageInfo.hasPreviousPage).toBe(true);
|
|
1621
|
+
|
|
1622
|
+
// Get previous page using cursor
|
|
1623
|
+
const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1624
|
+
last: 3,
|
|
1625
|
+
before: lastPage.pageInfo.startCursor!,
|
|
1626
|
+
pagination: {
|
|
1627
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1628
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1629
|
+
},
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
expect(previousPage.edges).toHaveLength(3);
|
|
1633
|
+
expect(previousPage.edges[0]?.node.getField('name')).toBe('Charlie');
|
|
1634
|
+
expect(previousPage.edges[1]?.node.getField('name')).toBe('David');
|
|
1635
|
+
expect(previousPage.edges[2]?.node.getField('name')).toBe('Eve');
|
|
1636
|
+
expect(previousPage.pageInfo.hasNextPage).toBe(false);
|
|
1637
|
+
expect(previousPage.pageInfo.hasPreviousPage).toBe(true);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('supports pagination with SQL where conditions', async () => {
|
|
1641
|
+
const vc = new ViewerContext(
|
|
1642
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// Query only entities with cats
|
|
1646
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1647
|
+
first: 2,
|
|
1648
|
+
where: sql`has_a_cat = ${true}`,
|
|
1649
|
+
pagination: {
|
|
1650
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1651
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1652
|
+
},
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
expect(page.edges).toHaveLength(2);
|
|
1656
|
+
expect(page.edges[0]?.node.getField('name')).toBe('Alice');
|
|
1657
|
+
expect(page.edges[0]?.node.getField('hasACat')).toBe(true);
|
|
1658
|
+
expect(page.edges[1]?.node.getField('name')).toBe('Charlie');
|
|
1659
|
+
expect(page.edges[1]?.node.getField('hasACat')).toBe(true);
|
|
1660
|
+
expect(page.pageInfo.hasNextPage).toBe(true);
|
|
1661
|
+
|
|
1662
|
+
// Get next page with same where condition
|
|
1663
|
+
const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1664
|
+
first: 2,
|
|
1665
|
+
after: page.pageInfo.endCursor!,
|
|
1666
|
+
where: sql`has_a_cat = ${true}`,
|
|
1667
|
+
pagination: {
|
|
1668
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1669
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1670
|
+
},
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
expect(nextPage.edges).toHaveLength(2);
|
|
1674
|
+
expect(nextPage.edges[0]?.node.getField('name')).toBe('Eve');
|
|
1675
|
+
expect(nextPage.edges[0]?.node.getField('hasACat')).toBe(true);
|
|
1676
|
+
expect(nextPage.edges[1]?.node.getField('name')).toBe('Grace');
|
|
1677
|
+
expect(nextPage.edges[1]?.node.getField('hasACat')).toBe(true);
|
|
1678
|
+
expect(nextPage.pageInfo.hasNextPage).toBe(false);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
it('supports pagination with multiple orderBy fields', async () => {
|
|
1682
|
+
const vc = new ViewerContext(
|
|
1683
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1687
|
+
first: 4,
|
|
1688
|
+
pagination: {
|
|
1689
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1690
|
+
orderBy: [
|
|
1691
|
+
{ fieldName: 'hasACat', order: OrderByOrdering.DESCENDING }, // true comes before false
|
|
1692
|
+
{ fieldName: 'name', order: OrderByOrdering.DESCENDING },
|
|
1693
|
+
{ fieldName: 'id', order: OrderByOrdering.DESCENDING },
|
|
1694
|
+
],
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// Entities with cats (true) come first, then sorted by name descending
|
|
1699
|
+
expect(page.edges).toHaveLength(4);
|
|
1700
|
+
expect(page.edges[0]?.node.getField('hasACat')).toBe(true);
|
|
1701
|
+
expect(page.edges[0]?.node.getField('name')).toBe('Grace');
|
|
1702
|
+
expect(page.edges[1]?.node.getField('hasACat')).toBe(true);
|
|
1703
|
+
expect(page.edges[1]?.node.getField('name')).toBe('Eve');
|
|
1704
|
+
expect(page.edges[2]?.node.getField('hasACat')).toBe(true);
|
|
1705
|
+
expect(page.edges[2]?.node.getField('name')).toBe('Charlie');
|
|
1706
|
+
expect(page.edges[3]?.node.getField('hasACat')).toBe(true);
|
|
1707
|
+
expect(page.edges[3]?.node.getField('name')).toBe('Alice');
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
it('supports pagination with fieldFragment orderBy', async () => {
|
|
1711
|
+
const vc = new ViewerContext(
|
|
1712
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1713
|
+
);
|
|
1714
|
+
|
|
1715
|
+
// Order by computed expression: cat owners first, then by name
|
|
1716
|
+
// Standard test data: Alice(cat), Bob, Charlie(cat), David, Eve(cat), Frank, Grace(cat), Henry
|
|
1717
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1718
|
+
first: 3,
|
|
1719
|
+
pagination: {
|
|
1720
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1721
|
+
orderBy: [
|
|
1722
|
+
{
|
|
1723
|
+
fieldFragment: sql`CASE WHEN has_a_cat = ${true} THEN ${0} ELSE ${1} END`,
|
|
1724
|
+
order: OrderByOrdering.ASCENDING,
|
|
1725
|
+
},
|
|
1726
|
+
{ fieldName: 'name', order: OrderByOrdering.ASCENDING },
|
|
1727
|
+
],
|
|
1728
|
+
},
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
// Cat owners alphabetically first: Alice, Charlie, Eve
|
|
1732
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
1733
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Alice');
|
|
1734
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBe('Charlie');
|
|
1735
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Eve');
|
|
1736
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
1737
|
+
|
|
1738
|
+
// Get second page using cursor
|
|
1739
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1740
|
+
first: 3,
|
|
1741
|
+
after: firstPage.pageInfo.endCursor!,
|
|
1742
|
+
pagination: {
|
|
1743
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1744
|
+
orderBy: [
|
|
1745
|
+
{
|
|
1746
|
+
fieldFragment: sql`CASE WHEN has_a_cat = ${true} THEN ${0} ELSE ${1} END`,
|
|
1747
|
+
order: OrderByOrdering.ASCENDING,
|
|
1748
|
+
},
|
|
1749
|
+
{ fieldName: 'name', order: OrderByOrdering.ASCENDING },
|
|
1750
|
+
],
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// Next cat owner, then non-cat-owners alphabetically: Grace, Bob, David
|
|
1755
|
+
expect(secondPage.edges).toHaveLength(3);
|
|
1756
|
+
expect(secondPage.edges[0]?.node.getField('name')).toBe('Grace');
|
|
1757
|
+
expect(secondPage.edges[1]?.node.getField('name')).toBe('Bob');
|
|
1758
|
+
expect(secondPage.edges[2]?.node.getField('name')).toBe('David');
|
|
1759
|
+
expect(secondPage.pageInfo.hasNextPage).toBe(true);
|
|
1760
|
+
|
|
1761
|
+
// Get third (last) page
|
|
1762
|
+
const thirdPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1763
|
+
first: 3,
|
|
1764
|
+
after: secondPage.pageInfo.endCursor!,
|
|
1765
|
+
pagination: {
|
|
1766
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1767
|
+
orderBy: [
|
|
1768
|
+
{
|
|
1769
|
+
fieldFragment: sql`CASE WHEN has_a_cat = ${true} THEN ${0} ELSE ${1} END`,
|
|
1770
|
+
order: OrderByOrdering.ASCENDING,
|
|
1771
|
+
},
|
|
1772
|
+
{ fieldName: 'name', order: OrderByOrdering.ASCENDING },
|
|
1773
|
+
],
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// Remaining non-cat-owners: Frank, Henry
|
|
1778
|
+
expect(thirdPage.edges).toHaveLength(2);
|
|
1779
|
+
expect(thirdPage.edges[0]?.node.getField('name')).toBe('Frank');
|
|
1780
|
+
expect(thirdPage.edges[1]?.node.getField('name')).toBe('Henry');
|
|
1781
|
+
expect(thirdPage.pageInfo.hasNextPage).toBe(false);
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
it('handles empty results correctly', async () => {
|
|
1785
|
+
const vc = new ViewerContext(
|
|
1786
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1787
|
+
);
|
|
1788
|
+
|
|
1789
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1790
|
+
first: 10,
|
|
1791
|
+
where: sql`name = ${'NonexistentName'}`,
|
|
1792
|
+
pagination: {
|
|
1793
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1794
|
+
orderBy: [],
|
|
1795
|
+
},
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
expect(page.edges).toHaveLength(0);
|
|
1799
|
+
expect(page.pageInfo.hasNextPage).toBe(false);
|
|
1800
|
+
expect(page.pageInfo.hasPreviousPage).toBe(false);
|
|
1801
|
+
expect(page.pageInfo.startCursor).toBeNull();
|
|
1802
|
+
expect(page.pageInfo.endCursor).toBeNull();
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
it('includes cursors for each edge', async () => {
|
|
1806
|
+
const vc = new ViewerContext(
|
|
1807
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1808
|
+
);
|
|
1809
|
+
|
|
1810
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1811
|
+
first: 3,
|
|
1812
|
+
pagination: {
|
|
1813
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1814
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1815
|
+
},
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
// Each edge should have a cursor
|
|
1819
|
+
expect(page.edges[0]?.cursor).toBeTruthy();
|
|
1820
|
+
expect(page.edges[1]?.cursor).toBeTruthy();
|
|
1821
|
+
expect(page.edges[2]?.cursor).toBeTruthy();
|
|
1822
|
+
|
|
1823
|
+
// Start from middle item
|
|
1824
|
+
const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1825
|
+
first: 2,
|
|
1826
|
+
after: page.edges[1]!.cursor,
|
|
1827
|
+
pagination: {
|
|
1828
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1829
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1830
|
+
},
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
expect(nextPage.edges).toHaveLength(2);
|
|
1834
|
+
expect(nextPage.edges[0]?.node.getField('name')).toBe('Charlie');
|
|
1835
|
+
expect(nextPage.edges[1]?.node.getField('name')).toBe('David');
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
it('derives postgres cursor fields from orderBy', async () => {
|
|
1839
|
+
const vc = new ViewerContext(
|
|
1840
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1841
|
+
);
|
|
1842
|
+
|
|
1843
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1844
|
+
first: 3,
|
|
1845
|
+
pagination: {
|
|
1846
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1847
|
+
orderBy: [{ fieldName: 'dateField', order: OrderByOrdering.ASCENDING }],
|
|
1848
|
+
},
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
expect(page.edges).toHaveLength(3);
|
|
1852
|
+
|
|
1853
|
+
// Navigate using cursor
|
|
1854
|
+
const nextPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1855
|
+
first: 3,
|
|
1856
|
+
after: page.pageInfo.endCursor!,
|
|
1857
|
+
pagination: {
|
|
1858
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1859
|
+
orderBy: [{ fieldName: 'dateField', order: OrderByOrdering.ASCENDING }],
|
|
1860
|
+
},
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
expect(nextPage.edges).toHaveLength(3);
|
|
1864
|
+
expect(nextPage.pageInfo.hasNextPage).toBe(true);
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
it('performs forward pagination with ascending order', async () => {
|
|
1869
|
+
const vc = new ViewerContext(
|
|
1870
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1871
|
+
);
|
|
1872
|
+
|
|
1873
|
+
// Create test data with names that sort in a specific order
|
|
1874
|
+
const entities = [];
|
|
1875
|
+
for (let i = 1; i <= 5; i++) {
|
|
1876
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
1877
|
+
.setField('name', `Z_Item_${i}`) // Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
1878
|
+
.createAsync();
|
|
1879
|
+
entities.push(entity);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Get first page with ASCENDING order
|
|
1883
|
+
// Sorted ascending: Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
1884
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1885
|
+
first: 3,
|
|
1886
|
+
pagination: {
|
|
1887
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1888
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1889
|
+
},
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
1893
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Z_Item_1');
|
|
1894
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBe('Z_Item_2');
|
|
1895
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Z_Item_3');
|
|
1896
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
1897
|
+
expect(firstPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1898
|
+
|
|
1899
|
+
// Get second page using cursor
|
|
1900
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1901
|
+
first: 3,
|
|
1902
|
+
after: firstPage.pageInfo.endCursor!,
|
|
1903
|
+
pagination: {
|
|
1904
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1905
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1906
|
+
},
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
// Remaining items in ascending order: Z_Item_4, Z_Item_5
|
|
1910
|
+
expect(secondPage.edges).toHaveLength(2);
|
|
1911
|
+
expect(secondPage.edges[0]?.node.getField('name')).toBe('Z_Item_4');
|
|
1912
|
+
expect(secondPage.edges[1]?.node.getField('name')).toBe('Z_Item_5');
|
|
1913
|
+
expect(secondPage.pageInfo.hasNextPage).toBe(false);
|
|
1914
|
+
expect(secondPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
it('performs backward pagination with ascending order', async () => {
|
|
1918
|
+
const vc = new ViewerContext(
|
|
1919
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1920
|
+
);
|
|
1921
|
+
|
|
1922
|
+
// Create test data with names that sort in a specific order
|
|
1923
|
+
const entities = [];
|
|
1924
|
+
for (let i = 1; i <= 5; i++) {
|
|
1925
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
1926
|
+
.setField('name', `Z_Item_${i}`) // Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
1927
|
+
.createAsync();
|
|
1928
|
+
entities.push(entity);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Test backward pagination with ASCENDING order
|
|
1932
|
+
// This internally flips ASCENDING to DESCENDING for the query
|
|
1933
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1934
|
+
last: 3,
|
|
1935
|
+
pagination: {
|
|
1936
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1937
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1938
|
+
},
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
// With `last: 3` and ASCENDING order, we get the last 3 items when sorted ascending
|
|
1942
|
+
// Sorted ascending: Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
1943
|
+
// Last 3: Z_Item_3, Z_Item_4, Z_Item_5
|
|
1944
|
+
expect(page.edges).toHaveLength(3);
|
|
1945
|
+
expect(page.edges[0]?.node.getField('name')).toBe('Z_Item_3');
|
|
1946
|
+
expect(page.edges[1]?.node.getField('name')).toBe('Z_Item_4');
|
|
1947
|
+
expect(page.edges[2]?.node.getField('name')).toBe('Z_Item_5');
|
|
1948
|
+
expect(page.pageInfo.hasPreviousPage).toBe(true);
|
|
1949
|
+
expect(page.pageInfo.hasNextPage).toBe(false);
|
|
1950
|
+
|
|
1951
|
+
// Get previous page using cursor
|
|
1952
|
+
const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1953
|
+
last: 3,
|
|
1954
|
+
before: page.pageInfo.startCursor!,
|
|
1955
|
+
pagination: {
|
|
1956
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1957
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
1958
|
+
},
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
// Remaining items in ascending order: Z_Item_1, Z_Item_2
|
|
1962
|
+
expect(previousPage.edges).toHaveLength(2);
|
|
1963
|
+
expect(previousPage.edges[0]?.node.getField('name')).toBe('Z_Item_1');
|
|
1964
|
+
expect(previousPage.edges[1]?.node.getField('name')).toBe('Z_Item_2');
|
|
1965
|
+
expect(previousPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1966
|
+
expect(previousPage.pageInfo.hasNextPage).toBe(false);
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
it('performs forward pagination with descending order', async () => {
|
|
1970
|
+
const vc = new ViewerContext(
|
|
1971
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
1972
|
+
);
|
|
1973
|
+
|
|
1974
|
+
// Create test data with names that sort in a specific order
|
|
1975
|
+
const entities = [];
|
|
1976
|
+
for (let i = 1; i <= 5; i++) {
|
|
1977
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
1978
|
+
.setField('name', `Z_Item_${i}`) // Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
1979
|
+
.createAsync();
|
|
1980
|
+
entities.push(entity);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Get first page with DESCENDING order
|
|
1984
|
+
// Sorted descending: Z_Item_5, Z_Item_4, Z_Item_3, Z_Item_2, Z_Item_1
|
|
1985
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
1986
|
+
first: 3,
|
|
1987
|
+
pagination: {
|
|
1988
|
+
strategy: PaginationStrategy.STANDARD,
|
|
1989
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.DESCENDING }],
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
1994
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Z_Item_5');
|
|
1995
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBe('Z_Item_4');
|
|
1996
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Z_Item_3');
|
|
1997
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
1998
|
+
expect(firstPage.pageInfo.hasPreviousPage).toBe(false);
|
|
1999
|
+
|
|
2000
|
+
// Get second page using cursor
|
|
2001
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2002
|
+
first: 3,
|
|
2003
|
+
after: firstPage.pageInfo.endCursor!,
|
|
2004
|
+
pagination: {
|
|
2005
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2006
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.DESCENDING }],
|
|
2007
|
+
},
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
// Remaining items in descending order: Z_Item_2, Z_Item_1
|
|
2011
|
+
expect(secondPage.edges).toHaveLength(2);
|
|
2012
|
+
expect(secondPage.edges[0]?.node.getField('name')).toBe('Z_Item_2');
|
|
2013
|
+
expect(secondPage.edges[1]?.node.getField('name')).toBe('Z_Item_1');
|
|
2014
|
+
expect(secondPage.pageInfo.hasNextPage).toBe(false);
|
|
2015
|
+
expect(secondPage.pageInfo.hasPreviousPage).toBe(false);
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
it('performs backward pagination with descending order', async () => {
|
|
2019
|
+
const vc = new ViewerContext(
|
|
2020
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2021
|
+
);
|
|
2022
|
+
|
|
2023
|
+
// Create test data with names that sort in a specific order
|
|
2024
|
+
|
|
2025
|
+
const entities = [];
|
|
2026
|
+
for (let i = 1; i <= 5; i++) {
|
|
2027
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
2028
|
+
.setField('name', `Z_Item_${i}`) // Z_Item_1, Z_Item_2, Z_Item_3, Z_Item_4, Z_Item_5
|
|
2029
|
+
.createAsync();
|
|
2030
|
+
entities.push(entity);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Test backward pagination with DESCENDING order
|
|
2034
|
+
// This internally flips DESCENDING to ASCENDING for the query
|
|
2035
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2036
|
+
last: 3,
|
|
2037
|
+
pagination: {
|
|
2038
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2039
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.DESCENDING }],
|
|
2040
|
+
},
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// With `last: 3` and DESCENDING order, we get the last 3 items when sorted descending
|
|
2044
|
+
// Sorted descending: Z_Item_5, Z_Item_4, Z_Item_3, Z_Item_2, Z_Item_1
|
|
2045
|
+
// Last 3: Z_Item_3, Z_Item_2, Z_Item_1
|
|
2046
|
+
expect(page.edges).toHaveLength(3);
|
|
2047
|
+
expect(page.edges[0]?.node.getField('name')).toBe('Z_Item_3');
|
|
2048
|
+
expect(page.edges[1]?.node.getField('name')).toBe('Z_Item_2');
|
|
2049
|
+
expect(page.edges[2]?.node.getField('name')).toBe('Z_Item_1');
|
|
2050
|
+
expect(page.pageInfo.hasPreviousPage).toBe(true);
|
|
2051
|
+
expect(page.pageInfo.hasNextPage).toBe(false);
|
|
2052
|
+
|
|
2053
|
+
// Get previous page using cursor
|
|
2054
|
+
const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2055
|
+
last: 3,
|
|
2056
|
+
before: page.pageInfo.startCursor!,
|
|
2057
|
+
pagination: {
|
|
2058
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2059
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.DESCENDING }],
|
|
2060
|
+
},
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
// Remaining items in descending order: Z_Item_5, Z_Item_4
|
|
2064
|
+
expect(previousPage.edges).toHaveLength(2);
|
|
2065
|
+
expect(previousPage.edges[0]?.node.getField('name')).toBe('Z_Item_5');
|
|
2066
|
+
expect(previousPage.edges[1]?.node.getField('name')).toBe('Z_Item_4');
|
|
2067
|
+
expect(previousPage.pageInfo.hasPreviousPage).toBe(false);
|
|
2068
|
+
expect(previousPage.pageInfo.hasNextPage).toBe(false);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
it('always includes ID field in orderBy for stability', async () => {
|
|
2072
|
+
const vc = new ViewerContext(
|
|
2073
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
// Create entities with duplicate values to test stability
|
|
2077
|
+
|
|
2078
|
+
const entities = [];
|
|
2079
|
+
for (let i = 1; i <= 6; i++) {
|
|
2080
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
2081
|
+
.setField('name', `Test${Math.floor((i - 1) / 2)}`) // Creates duplicates: Test0, Test0, Test1, Test1, Test2, Test2
|
|
2082
|
+
.setField('hasACat', i % 2 === 0)
|
|
2083
|
+
.createAsync();
|
|
2084
|
+
entities.push(entity);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// Pagination with only name in orderBy - ID should be added automatically for stability
|
|
2088
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2089
|
+
first: 3,
|
|
2090
|
+
pagination: {
|
|
2091
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2092
|
+
orderBy: [],
|
|
2093
|
+
},
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
2097
|
+
|
|
2098
|
+
// Get second page
|
|
2099
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2100
|
+
first: 3,
|
|
2101
|
+
after: firstPage.pageInfo.endCursor!,
|
|
2102
|
+
pagination: {
|
|
2103
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2104
|
+
orderBy: [],
|
|
2105
|
+
},
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
expect(secondPage.edges).toHaveLength(3);
|
|
2109
|
+
|
|
2110
|
+
// Ensure no overlap between pages (stability check)
|
|
2111
|
+
const firstPageIds = firstPage.edges.map((e) => e.node.getID());
|
|
2112
|
+
const secondPageIds = secondPage.edges.map((e) => e.node.getID());
|
|
2113
|
+
const intersection = firstPageIds.filter((id) => secondPageIds.includes(id));
|
|
2114
|
+
expect(intersection).toHaveLength(0);
|
|
2115
|
+
|
|
2116
|
+
// Test with explicit ID in orderBy (shouldn't duplicate)
|
|
2117
|
+
const pageWithExplicitId = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2118
|
+
first: 3,
|
|
2119
|
+
pagination: {
|
|
2120
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2121
|
+
orderBy: [],
|
|
2122
|
+
},
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
expect(pageWithExplicitId.edges).toHaveLength(3);
|
|
2126
|
+
});
|
|
625
2127
|
|
|
2128
|
+
it('throws error for invalid cursor format', async () => {
|
|
2129
|
+
const vc = new ViewerContext(
|
|
2130
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
// Try with completely invalid cursor
|
|
626
2134
|
await expect(
|
|
627
|
-
|
|
628
|
-
|
|
2135
|
+
PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2136
|
+
first: 10,
|
|
2137
|
+
after: 'not-a-valid-cursor',
|
|
2138
|
+
pagination: {
|
|
2139
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2140
|
+
orderBy: [],
|
|
2141
|
+
},
|
|
2142
|
+
}),
|
|
2143
|
+
).rejects.toThrow('Failed to decode cursor');
|
|
2144
|
+
|
|
2145
|
+
// Try with valid base64 but invalid JSON
|
|
2146
|
+
const invalidJsonCursor = Buffer.from('not json').toString('base64url');
|
|
2147
|
+
await expect(
|
|
2148
|
+
PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2149
|
+
first: 10,
|
|
2150
|
+
after: invalidJsonCursor,
|
|
2151
|
+
pagination: {
|
|
2152
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2153
|
+
orderBy: [],
|
|
2154
|
+
},
|
|
2155
|
+
}),
|
|
2156
|
+
).rejects.toThrow('Failed to decode cursor');
|
|
2157
|
+
|
|
2158
|
+
// Try with valid JSON but missing required fields
|
|
2159
|
+
const missingFieldsCursor = Buffer.from(JSON.stringify({ some: 'field' })).toString(
|
|
2160
|
+
'base64url',
|
|
2161
|
+
);
|
|
629
2162
|
await expect(
|
|
630
|
-
|
|
631
|
-
|
|
2163
|
+
PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2164
|
+
first: 10,
|
|
2165
|
+
after: missingFieldsCursor,
|
|
2166
|
+
pagination: {
|
|
2167
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2168
|
+
orderBy: [],
|
|
2169
|
+
},
|
|
2170
|
+
}),
|
|
2171
|
+
).rejects.toThrow("Cursor is missing required 'id' field.");
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
it('performs pagination with both loader types', async () => {
|
|
2175
|
+
const vc = new ViewerContext(
|
|
2176
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2177
|
+
);
|
|
2178
|
+
|
|
2179
|
+
// Create entities with different names
|
|
2180
|
+
const names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'];
|
|
2181
|
+
for (const name of names) {
|
|
2182
|
+
await PostgresTestEntity.creator(vc).setField('name', name).createAsync();
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// Test with enforcing loader (standard pagination)
|
|
2186
|
+
const pageEnforced = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2187
|
+
first: 4,
|
|
2188
|
+
pagination: {
|
|
2189
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2190
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
2191
|
+
},
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
// Should return entities directly
|
|
2195
|
+
expect(pageEnforced.edges).toHaveLength(4);
|
|
2196
|
+
expect(pageEnforced.edges[0]?.node.getField('name')).toBe('Alice');
|
|
2197
|
+
expect(pageEnforced.edges[1]?.node.getField('name')).toBe('Bob');
|
|
2198
|
+
expect(pageEnforced.edges[2]?.node.getField('name')).toBe('Charlie');
|
|
2199
|
+
expect(pageEnforced.edges[3]?.node.getField('name')).toBe('David');
|
|
2200
|
+
|
|
2201
|
+
// Test pagination continues correctly
|
|
2202
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2203
|
+
first: 4,
|
|
2204
|
+
after: pageEnforced.pageInfo.endCursor!,
|
|
2205
|
+
pagination: {
|
|
2206
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2207
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
2208
|
+
},
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
expect(secondPage.edges).toHaveLength(2); // Only 2 entities left
|
|
2212
|
+
expect(secondPage.edges[0]?.node.getField('name')).toBe('Eve');
|
|
2213
|
+
expect(secondPage.edges[1]?.node.getField('name')).toBe('Frank');
|
|
2214
|
+
|
|
2215
|
+
// Test with authorization result-based loader
|
|
2216
|
+
// Note: Currently loadPageWithSearchAsync with knexLoaderWithAuthorizationResults
|
|
2217
|
+
// returns entities directly, not Result objects (unlike loadManyBySQL)
|
|
2218
|
+
const pageWithAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
|
|
2219
|
+
vc,
|
|
2220
|
+
).loadPageAsync({
|
|
2221
|
+
first: 3,
|
|
2222
|
+
pagination: {
|
|
2223
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2224
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
2225
|
+
},
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
expect(pageWithAuth.edges).toHaveLength(3);
|
|
2229
|
+
// These are entities, not Result objects in the current implementation
|
|
2230
|
+
expect(pageWithAuth.edges[0]?.node.getField('name')).toBe('Alice');
|
|
2231
|
+
expect(pageWithAuth.edges[1]?.node.getField('name')).toBe('Bob');
|
|
2232
|
+
expect(pageWithAuth.edges[2]?.node.getField('name')).toBe('Charlie');
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
it('correctly handles hasMore flag when filtering unauthorized entities', async () => {
|
|
2236
|
+
const vc = new ViewerContext(
|
|
2237
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2238
|
+
);
|
|
2239
|
+
|
|
2240
|
+
// Create exactly 6 entities
|
|
2241
|
+
for (let i = 1; i <= 6; i++) {
|
|
2242
|
+
await PostgresTestEntity.creator(vc).setField('name', `Entity${i}`).createAsync();
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Load with limit 5 - should have hasNextPage=true
|
|
2246
|
+
const page1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2247
|
+
first: 5,
|
|
2248
|
+
pagination: {
|
|
2249
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2250
|
+
orderBy: [],
|
|
2251
|
+
},
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
expect(page1.edges).toHaveLength(5);
|
|
2255
|
+
expect(page1.pageInfo.hasNextPage).toBe(true);
|
|
2256
|
+
|
|
2257
|
+
// Load the last entity
|
|
2258
|
+
const page2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2259
|
+
first: 5,
|
|
2260
|
+
after: page1.pageInfo.endCursor!,
|
|
2261
|
+
pagination: {
|
|
2262
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2263
|
+
orderBy: [],
|
|
2264
|
+
},
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
expect(page2.edges).toHaveLength(1);
|
|
2268
|
+
expect(page2.pageInfo.hasNextPage).toBe(false);
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
it('supports forward pagination with NULLS FIRST on ASC', async () => {
|
|
2272
|
+
const vc = new ViewerContext(
|
|
2273
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2274
|
+
);
|
|
2275
|
+
|
|
2276
|
+
// Create entities: some with null names, some with non-null names
|
|
2277
|
+
await PostgresTestEntity.creator(vc).setField('name', null).createAsync();
|
|
2278
|
+
await PostgresTestEntity.creator(vc).setField('name', null).createAsync();
|
|
2279
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Alice').createAsync();
|
|
2280
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Bob').createAsync();
|
|
2281
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Charlie').createAsync();
|
|
2282
|
+
|
|
2283
|
+
// ASC NULLS FIRST means nulls come first, then ascending values.
|
|
2284
|
+
// This overrides the PostgreSQL default of NULLS LAST for ASC.
|
|
2285
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2286
|
+
first: 3,
|
|
2287
|
+
pagination: {
|
|
2288
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2289
|
+
orderBy: [
|
|
2290
|
+
{
|
|
2291
|
+
fieldName: 'name',
|
|
2292
|
+
order: OrderByOrdering.ASCENDING,
|
|
2293
|
+
nulls: NullsOrdering.FIRST,
|
|
2294
|
+
},
|
|
2295
|
+
],
|
|
2296
|
+
},
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
2300
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBeNull();
|
|
2301
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBeNull();
|
|
2302
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Alice');
|
|
2303
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
it('supports forward pagination with NULLS LAST on DESC', async () => {
|
|
2307
|
+
const vc = new ViewerContext(
|
|
2308
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
// Create entities: some with null names, some with non-null names
|
|
2312
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Alice').createAsync();
|
|
2313
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Bob').createAsync();
|
|
2314
|
+
await PostgresTestEntity.creator(vc).setField('name', 'Charlie').createAsync();
|
|
2315
|
+
await PostgresTestEntity.creator(vc).setField('name', null).createAsync();
|
|
2316
|
+
await PostgresTestEntity.creator(vc).setField('name', null).createAsync();
|
|
2317
|
+
|
|
2318
|
+
// DESC NULLS LAST means descending values first, then nulls last.
|
|
2319
|
+
// This overrides the PostgreSQL default of NULLS FIRST for DESC.
|
|
2320
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2321
|
+
first: 3,
|
|
2322
|
+
pagination: {
|
|
2323
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2324
|
+
orderBy: [
|
|
2325
|
+
{
|
|
2326
|
+
fieldName: 'name',
|
|
2327
|
+
order: OrderByOrdering.DESCENDING,
|
|
2328
|
+
nulls: NullsOrdering.LAST,
|
|
2329
|
+
},
|
|
2330
|
+
],
|
|
2331
|
+
},
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
expect(firstPage.edges).toHaveLength(3);
|
|
2335
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Charlie');
|
|
2336
|
+
expect(firstPage.edges[1]?.node.getField('name')).toBe('Bob');
|
|
2337
|
+
expect(firstPage.edges[2]?.node.getField('name')).toBe('Alice');
|
|
2338
|
+
expect(firstPage.pageInfo.hasNextPage).toBe(true);
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
it('supports backward pagination with nulls ordering by flipping nulls direction', async () => {
|
|
2342
|
+
const vc = new ViewerContext(
|
|
2343
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2344
|
+
);
|
|
2345
|
+
|
|
2346
|
+
// Use non-null entities only to avoid the pre-existing limitation where
|
|
2347
|
+
// PostgreSQL tuple comparison evaluates to NULL when any element is NULL,
|
|
2348
|
+
// breaking cursor-based pagination across NULL boundaries.
|
|
2349
|
+
for (const name of ['Alice', 'Bob', 'Charlie', 'David', 'Eve']) {
|
|
2350
|
+
await PostgresTestEntity.creator(vc).setField('name', name).createAsync();
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// Backward pagination with ASC NULLS FIRST.
|
|
2354
|
+
// Internally this flips to DESC NULLS LAST (via flipNullsOrderingSpread),
|
|
2355
|
+
// fetches in that order, then reverses to present ASC NULLS FIRST order.
|
|
2356
|
+
const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2357
|
+
last: 3,
|
|
2358
|
+
pagination: {
|
|
2359
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2360
|
+
orderBy: [
|
|
2361
|
+
{
|
|
2362
|
+
fieldName: 'name',
|
|
2363
|
+
order: OrderByOrdering.ASCENDING,
|
|
2364
|
+
nulls: NullsOrdering.FIRST,
|
|
2365
|
+
},
|
|
2366
|
+
],
|
|
2367
|
+
},
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
// Last 3 in ASC order: Charlie, David, Eve
|
|
2371
|
+
expect(lastPage.edges).toHaveLength(3);
|
|
2372
|
+
expect(lastPage.edges[0]?.node.getField('name')).toBe('Charlie');
|
|
2373
|
+
expect(lastPage.edges[1]?.node.getField('name')).toBe('David');
|
|
2374
|
+
expect(lastPage.edges[2]?.node.getField('name')).toBe('Eve');
|
|
2375
|
+
expect(lastPage.pageInfo.hasPreviousPage).toBe(true);
|
|
2376
|
+
|
|
2377
|
+
// Continue backward with cursor: get the previous page before Charlie.
|
|
2378
|
+
// Cursor is on a non-null row so tuple comparison works correctly.
|
|
2379
|
+
const previousPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2380
|
+
last: 3,
|
|
2381
|
+
before: lastPage.pageInfo.startCursor!,
|
|
2382
|
+
pagination: {
|
|
2383
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2384
|
+
orderBy: [
|
|
2385
|
+
{
|
|
2386
|
+
fieldName: 'name',
|
|
2387
|
+
order: OrderByOrdering.ASCENDING,
|
|
2388
|
+
nulls: NullsOrdering.FIRST,
|
|
2389
|
+
},
|
|
2390
|
+
],
|
|
2391
|
+
},
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
expect(previousPage.edges).toHaveLength(2);
|
|
2395
|
+
expect(previousPage.edges[0]?.node.getField('name')).toBe('Alice');
|
|
2396
|
+
expect(previousPage.edges[1]?.node.getField('name')).toBe('Bob');
|
|
2397
|
+
expect(previousPage.pageInfo.hasPreviousPage).toBe(false);
|
|
2398
|
+
|
|
2399
|
+
// Also test DESC NULLS LAST backward pagination (without cursor).
|
|
2400
|
+
// This exercises flipNullsOrderingSpread: DESC NULLS LAST flips to ASC NULLS FIRST.
|
|
2401
|
+
const lastPageDesc = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2402
|
+
last: 3,
|
|
2403
|
+
pagination: {
|
|
2404
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2405
|
+
orderBy: [
|
|
2406
|
+
{
|
|
2407
|
+
fieldName: 'name',
|
|
2408
|
+
order: OrderByOrdering.DESCENDING,
|
|
2409
|
+
nulls: NullsOrdering.LAST,
|
|
2410
|
+
},
|
|
2411
|
+
],
|
|
2412
|
+
},
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
// Last 3 in DESC order: Charlie, Bob, Alice
|
|
2416
|
+
expect(lastPageDesc.edges).toHaveLength(3);
|
|
2417
|
+
expect(lastPageDesc.edges[0]?.node.getField('name')).toBe('Charlie');
|
|
2418
|
+
expect(lastPageDesc.edges[1]?.node.getField('name')).toBe('Bob');
|
|
2419
|
+
expect(lastPageDesc.edges[2]?.node.getField('name')).toBe('Alice');
|
|
2420
|
+
expect(lastPageDesc.pageInfo.hasPreviousPage).toBe(true);
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
it('performs paginated search with both loader types', async () => {
|
|
2424
|
+
const vc = new ViewerContext(
|
|
2425
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2426
|
+
);
|
|
2427
|
+
|
|
2428
|
+
// Enable pg_trgm extension for trigram similarity
|
|
2429
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
2430
|
+
|
|
2431
|
+
// Create test data with searchable names and mixed attributes
|
|
2432
|
+
const testData = [
|
|
2433
|
+
{ name: 'Alice Johnson', hasACat: true, hasADog: false },
|
|
2434
|
+
{ name: 'Bob Smith', hasACat: false, hasADog: true },
|
|
2435
|
+
{ name: 'Charlie Johnson', hasACat: true, hasADog: false },
|
|
2436
|
+
{ name: 'David Smith', hasACat: false, hasADog: false },
|
|
2437
|
+
{ name: 'Eve Thompson', hasACat: true, hasADog: true },
|
|
2438
|
+
{ name: 'Frank Johnson', hasACat: false, hasADog: true },
|
|
2439
|
+
];
|
|
2440
|
+
|
|
2441
|
+
for (const data of testData) {
|
|
2442
|
+
await PostgresTestEntity.creator(vc)
|
|
2443
|
+
.setField('name', data.name)
|
|
2444
|
+
.setField('label', data.name)
|
|
2445
|
+
.setField('hasACat', data.hasACat)
|
|
2446
|
+
.setField('hasADog', data.hasADog)
|
|
2447
|
+
.createAsync();
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Test 1: Regular loader with ILIKE search
|
|
2451
|
+
const iLikeSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2452
|
+
first: 2,
|
|
2453
|
+
pagination: {
|
|
2454
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2455
|
+
term: 'Johnson',
|
|
2456
|
+
fields: ['label'],
|
|
2457
|
+
},
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
expect(iLikeSearchRegular.edges).toHaveLength(2);
|
|
2461
|
+
expect(iLikeSearchRegular.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2462
|
+
expect(iLikeSearchRegular.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
|
|
2463
|
+
expect(iLikeSearchRegular.pageInfo.hasNextPage).toBe(true);
|
|
2464
|
+
|
|
2465
|
+
// Test 2: Authorization result loader with same ILIKE search
|
|
2466
|
+
const iLikeSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
|
|
2467
|
+
vc,
|
|
2468
|
+
).loadPageAsync({
|
|
2469
|
+
first: 2,
|
|
2470
|
+
pagination: {
|
|
2471
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2472
|
+
term: 'Johnson',
|
|
2473
|
+
fields: ['label'],
|
|
2474
|
+
},
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
expect(iLikeSearchAuth.edges).toHaveLength(2);
|
|
2478
|
+
// Authorization loader returns entities directly, not Result objects
|
|
2479
|
+
expect(iLikeSearchAuth.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2480
|
+
expect(iLikeSearchAuth.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
|
|
2481
|
+
expect(iLikeSearchAuth.pageInfo.hasNextPage).toBe(true);
|
|
2482
|
+
|
|
2483
|
+
// Test 3: Regular loader with TRIGRAM search
|
|
2484
|
+
const trigramSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2485
|
+
first: 3,
|
|
2486
|
+
pagination: {
|
|
2487
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
2488
|
+
term: 'Jonson', // Intentional misspelling to test similarity
|
|
2489
|
+
fields: ['label'],
|
|
2490
|
+
threshold: 0.2,
|
|
2491
|
+
},
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
// Should find Johnson names due to similarity
|
|
2495
|
+
expect(trigramSearchRegular.edges.length).toBeGreaterThan(0);
|
|
2496
|
+
const foundNames = trigramSearchRegular.edges.map((e) => e.node.getField('name'));
|
|
2497
|
+
expect(foundNames).toContain('Alice Johnson');
|
|
2498
|
+
expect(foundNames).toContain('Charlie Johnson');
|
|
2499
|
+
expect(foundNames).toContain('Frank Johnson');
|
|
2500
|
+
|
|
2501
|
+
// Test 4: Authorization result loader with TRIGRAM search
|
|
2502
|
+
const trigramSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
|
|
2503
|
+
vc,
|
|
2504
|
+
).loadPageAsync({
|
|
2505
|
+
first: 3,
|
|
2506
|
+
pagination: {
|
|
2507
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
2508
|
+
term: 'Jonson', // Intentional misspelling
|
|
2509
|
+
fields: ['label'],
|
|
2510
|
+
threshold: 0.2,
|
|
2511
|
+
},
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
expect(trigramSearchAuth.edges.length).toBeGreaterThan(0);
|
|
2515
|
+
const foundNamesAuth = trigramSearchAuth.edges.map((e) => e.node.getField('name'));
|
|
2516
|
+
expect(foundNamesAuth).toContain('Alice Johnson');
|
|
2517
|
+
expect(foundNamesAuth).toContain('Charlie Johnson');
|
|
2518
|
+
expect(foundNamesAuth).toContain('Frank Johnson');
|
|
2519
|
+
|
|
2520
|
+
// Test 5: Test pagination with cursor for both loader types
|
|
2521
|
+
const firstPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2522
|
+
first: 1,
|
|
2523
|
+
pagination: {
|
|
2524
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2525
|
+
term: 'Smith',
|
|
2526
|
+
fields: ['label'],
|
|
2527
|
+
},
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
expect(firstPageRegular.edges).toHaveLength(1);
|
|
2531
|
+
expect(firstPageRegular.edges[0]?.node.getField('name')).toBe('Bob Smith');
|
|
2532
|
+
|
|
2533
|
+
const secondPageRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2534
|
+
first: 1,
|
|
2535
|
+
after: firstPageRegular.pageInfo.endCursor!,
|
|
2536
|
+
pagination: {
|
|
2537
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2538
|
+
term: 'Smith',
|
|
2539
|
+
fields: ['label'],
|
|
2540
|
+
},
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
expect(secondPageRegular.edges).toHaveLength(1);
|
|
2544
|
+
expect(secondPageRegular.edges[0]?.node.getField('name')).toBe('David Smith');
|
|
2545
|
+
|
|
2546
|
+
// Test 6: Combine search with WHERE filter for both loaders
|
|
2547
|
+
const filteredSearchRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2548
|
+
first: 10,
|
|
2549
|
+
where: sql`has_a_cat = ${true}`,
|
|
2550
|
+
pagination: {
|
|
2551
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2552
|
+
term: 'Johnson',
|
|
2553
|
+
fields: ['label'],
|
|
2554
|
+
},
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
// Only Alice Johnson and Charlie Johnson have cats
|
|
2558
|
+
expect(filteredSearchRegular.edges).toHaveLength(2);
|
|
2559
|
+
expect(filteredSearchRegular.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2560
|
+
expect(filteredSearchRegular.edges[0]?.node.getField('hasACat')).toBe(true);
|
|
2561
|
+
expect(filteredSearchRegular.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
|
|
2562
|
+
expect(filteredSearchRegular.edges[1]?.node.getField('hasACat')).toBe(true);
|
|
2563
|
+
|
|
2564
|
+
const filteredSearchAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
|
|
2565
|
+
vc,
|
|
2566
|
+
).loadPageAsync({
|
|
2567
|
+
first: 10,
|
|
2568
|
+
where: sql`has_a_cat = ${true}`,
|
|
2569
|
+
pagination: {
|
|
2570
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2571
|
+
term: 'Johnson',
|
|
2572
|
+
fields: ['label'],
|
|
2573
|
+
},
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
expect(filteredSearchAuth.edges).toHaveLength(2);
|
|
2577
|
+
expect(filteredSearchAuth.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2578
|
+
expect(filteredSearchAuth.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
|
|
2579
|
+
|
|
2580
|
+
// Test 7: Test with both loader types
|
|
2581
|
+
const withRegular = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2582
|
+
first: 1,
|
|
2583
|
+
pagination: {
|
|
2584
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2585
|
+
term: 'Johnson',
|
|
2586
|
+
fields: ['label'],
|
|
2587
|
+
},
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2590
|
+
expect(withRegular.edges).toHaveLength(1);
|
|
2591
|
+
|
|
2592
|
+
const withAuth = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
|
|
2593
|
+
vc,
|
|
2594
|
+
).loadPageAsync({
|
|
2595
|
+
first: 1,
|
|
2596
|
+
pagination: {
|
|
2597
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2598
|
+
term: 'Johnson',
|
|
2599
|
+
fields: ['label'],
|
|
2600
|
+
},
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
expect(withAuth.edges).toHaveLength(1);
|
|
2604
|
+
});
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
it('returns empty page when cursor entity no longer exists', async () => {
|
|
2608
|
+
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
2609
|
+
|
|
2610
|
+
// Create test entities
|
|
2611
|
+
const names = ['Alice', 'Bob', 'Charlie', 'David', 'Eve'];
|
|
2612
|
+
for (const name of names) {
|
|
2613
|
+
await PostgresTestEntity.creator(vc).setField('name', name).createAsync();
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Get first page and capture cursor pointing to a specific entity
|
|
2617
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2618
|
+
first: 2,
|
|
2619
|
+
pagination: {
|
|
2620
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2621
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
2622
|
+
},
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
expect(firstPage.edges).toHaveLength(2);
|
|
2626
|
+
const cursorEntityNode = firstPage.edges[1]!.node; // 'Bob'
|
|
2627
|
+
const cursor = firstPage.pageInfo.endCursor!;
|
|
2628
|
+
|
|
2629
|
+
// Delete the entity that the cursor refers to
|
|
2630
|
+
await PostgresTestEntity.deleter(cursorEntityNode).deleteAsync();
|
|
2631
|
+
|
|
2632
|
+
// Paginate using the cursor of the now-deleted entity
|
|
2633
|
+
const result = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2634
|
+
first: 10,
|
|
2635
|
+
after: cursor,
|
|
2636
|
+
pagination: {
|
|
2637
|
+
strategy: PaginationStrategy.STANDARD,
|
|
2638
|
+
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
|
|
2639
|
+
},
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
expect(result.edges).toEqual([]);
|
|
2643
|
+
expect(result.pageInfo).toEqual({
|
|
2644
|
+
hasNextPage: false,
|
|
2645
|
+
hasPreviousPage: false,
|
|
2646
|
+
startCursor: null,
|
|
2647
|
+
endCursor: null,
|
|
2648
|
+
});
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
describe(PaginationStrategy.ILIKE_SEARCH, () => {
|
|
2652
|
+
it('supports search with ILIKE strategy', async () => {
|
|
2653
|
+
const vc = new ViewerContext(
|
|
2654
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2655
|
+
);
|
|
2656
|
+
|
|
2657
|
+
// Create test data with searchable names
|
|
2658
|
+
const names = [
|
|
2659
|
+
'Alice Johnson',
|
|
2660
|
+
'Bob Smith',
|
|
2661
|
+
'Charlie Brown',
|
|
2662
|
+
'David Smith',
|
|
2663
|
+
'Eve Johnson',
|
|
2664
|
+
'Frank Miller',
|
|
2665
|
+
];
|
|
2666
|
+
for (let i = 0; i < names.length; i++) {
|
|
2667
|
+
await PostgresTestEntity.creator(vc)
|
|
2668
|
+
.setField('name', names[i]!)
|
|
2669
|
+
.setField('label', names[i]!)
|
|
2670
|
+
.setField('hasACat', i % 2 === 0)
|
|
2671
|
+
.createAsync();
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Search for names containing "Johnson"
|
|
2675
|
+
const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2676
|
+
first: 10,
|
|
2677
|
+
pagination: {
|
|
2678
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2679
|
+
term: 'Johnson',
|
|
2680
|
+
fields: ['label'],
|
|
2681
|
+
},
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
expect(searchResults.edges).toHaveLength(2);
|
|
2685
|
+
expect(searchResults.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2686
|
+
expect(searchResults.edges[1]?.node.getField('name')).toBe('Eve Johnson');
|
|
2687
|
+
|
|
2688
|
+
// Search for names containing "Smith" with pagination
|
|
2689
|
+
const smithPage1 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2690
|
+
first: 1,
|
|
2691
|
+
pagination: {
|
|
2692
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2693
|
+
term: 'Smith',
|
|
2694
|
+
fields: ['label'],
|
|
2695
|
+
},
|
|
2696
|
+
});
|
|
2697
|
+
|
|
2698
|
+
expect(smithPage1.edges).toHaveLength(1);
|
|
2699
|
+
expect(smithPage1.edges[0]?.node.getField('name')).toBe('Bob Smith');
|
|
2700
|
+
expect(smithPage1.pageInfo.hasNextPage).toBe(true);
|
|
2701
|
+
|
|
2702
|
+
// Get next page
|
|
2703
|
+
const smithPage2 = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2704
|
+
first: 1,
|
|
2705
|
+
after: smithPage1.pageInfo.endCursor!,
|
|
2706
|
+
pagination: {
|
|
2707
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2708
|
+
term: 'Smith',
|
|
2709
|
+
fields: ['label'],
|
|
2710
|
+
},
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
expect(smithPage2.edges).toHaveLength(1);
|
|
2714
|
+
expect(smithPage2.edges[0]?.node.getField('name')).toBe('David Smith');
|
|
2715
|
+
expect(smithPage2.pageInfo.hasNextPage).toBe(false);
|
|
2716
|
+
|
|
2717
|
+
// Test partial match (case insensitive)
|
|
2718
|
+
const partialMatch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2719
|
+
first: 10,
|
|
2720
|
+
pagination: {
|
|
2721
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2722
|
+
term: 'john',
|
|
2723
|
+
fields: ['label'],
|
|
2724
|
+
},
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
expect(partialMatch.edges).toHaveLength(2);
|
|
2728
|
+
expect(partialMatch.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2729
|
+
expect(partialMatch.edges[1]?.node.getField('name')).toBe('Eve Johnson');
|
|
2730
|
+
|
|
2731
|
+
// Test search with WHERE clause
|
|
2732
|
+
const combinedFilter = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2733
|
+
first: 10,
|
|
2734
|
+
where: sql`has_a_cat = ${true}`,
|
|
2735
|
+
pagination: {
|
|
2736
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2737
|
+
term: 'Johnson',
|
|
2738
|
+
fields: ['label'],
|
|
2739
|
+
},
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
// Both Alice Johnson (index 0) and Eve Johnson (index 4) have cats
|
|
2743
|
+
expect(combinedFilter.edges).toHaveLength(2);
|
|
2744
|
+
expect(combinedFilter.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
2745
|
+
expect(combinedFilter.edges[0]?.node.getField('hasACat')).toBe(true);
|
|
2746
|
+
expect(combinedFilter.edges[1]?.node.getField('name')).toBe('Eve Johnson');
|
|
2747
|
+
expect(combinedFilter.edges[1]?.node.getField('hasACat')).toBe(true);
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
it('search with ILIKE strategy works with forward and backward pagination', async () => {
|
|
2751
|
+
const vc = new ViewerContext(
|
|
2752
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2753
|
+
);
|
|
2754
|
+
|
|
2755
|
+
// Create test data
|
|
2756
|
+
const names = ['Apple', 'Application', 'Apply', 'Banana', 'Cherry', 'Pineapple'];
|
|
2757
|
+
for (const name of names) {
|
|
2758
|
+
await PostgresTestEntity.creator(vc)
|
|
2759
|
+
.setField('name', name)
|
|
2760
|
+
.setField('label', name)
|
|
2761
|
+
.createAsync();
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// Forward pagination with ILIKE search
|
|
2765
|
+
const forwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2766
|
+
first: 2,
|
|
2767
|
+
pagination: {
|
|
2768
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2769
|
+
term: 'app',
|
|
2770
|
+
fields: ['label'],
|
|
2771
|
+
},
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2774
|
+
expect(forwardPage.edges).toHaveLength(2);
|
|
2775
|
+
const forwardNames = forwardPage.edges.map((e) => e.node.getField('name'));
|
|
2776
|
+
// Should match Apple, Application, Apply, Pineapple (case-insensitive)
|
|
2777
|
+
forwardNames.forEach((name) => {
|
|
2778
|
+
expect(name?.toLowerCase()).toContain('app');
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
// Backward pagination with ILIKE search
|
|
2782
|
+
const backwardPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2783
|
+
last: 2,
|
|
2784
|
+
pagination: {
|
|
2785
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2786
|
+
term: 'app',
|
|
2787
|
+
fields: ['label'],
|
|
2788
|
+
},
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
expect(backwardPage.edges).toHaveLength(2);
|
|
2792
|
+
const backwardNames = backwardPage.edges.map((e) => e.node.getField('name'));
|
|
2793
|
+
backwardNames.forEach((name) => {
|
|
2794
|
+
expect(name?.toLowerCase()).toContain('app');
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
// Verify complete coverage with cursors
|
|
2798
|
+
const allResults: string[] = [];
|
|
2799
|
+
let cursor: string | undefined;
|
|
2800
|
+
let hasNext = true;
|
|
2801
|
+
|
|
2802
|
+
while (hasNext) {
|
|
2803
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2804
|
+
first: 10,
|
|
2805
|
+
...(cursor && { after: cursor }),
|
|
2806
|
+
pagination: {
|
|
2807
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2808
|
+
term: 'app',
|
|
2809
|
+
fields: ['label'],
|
|
2810
|
+
},
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
allResults.push(
|
|
2814
|
+
...page.edges
|
|
2815
|
+
.map((e) => e.node.getField('name'))
|
|
2816
|
+
.filter((n): n is string => n !== null),
|
|
2817
|
+
);
|
|
2818
|
+
cursor = page.pageInfo.endCursor ?? undefined;
|
|
2819
|
+
hasNext = page.pageInfo.hasNextPage;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// Should find all names containing 'app' (case-insensitive)
|
|
2823
|
+
expect(allResults).toContain('Apple');
|
|
2824
|
+
expect(allResults).toContain('Application');
|
|
2825
|
+
expect(allResults).toContain('Apply');
|
|
2826
|
+
expect(allResults).toContain('Pineapple');
|
|
2827
|
+
expect(allResults).not.toContain('Banana');
|
|
2828
|
+
expect(allResults).not.toContain('Cherry');
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
it('verifies ILIKE search cursor pagination works correctly with ORDER BY', async () => {
|
|
2832
|
+
const vc = new ViewerContext(
|
|
2833
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2834
|
+
);
|
|
2835
|
+
|
|
2836
|
+
// Create test data with many matching records
|
|
2837
|
+
const testNames = [];
|
|
2838
|
+
for (let i = 0; i < 20; i++) {
|
|
2839
|
+
testNames.push(`Test${i.toString().padStart(2, '0')}_Pattern`);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// Shuffle the array to create in random order
|
|
2843
|
+
const shuffled = [...testNames].sort(() => Math.random() - 0.5);
|
|
2844
|
+
|
|
2845
|
+
// Create entities in shuffled order
|
|
2846
|
+
const createdEntities = [];
|
|
2847
|
+
for (const name of shuffled) {
|
|
2848
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
2849
|
+
.setField('name', name)
|
|
2850
|
+
.setField('label', name)
|
|
2851
|
+
.setField('hasACat', Math.random() > 0.5)
|
|
2852
|
+
.createAsync();
|
|
2853
|
+
createdEntities.push(entity);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Paginate through all results with ILIKE search, collecting all names
|
|
2857
|
+
const allNames: string[] = [];
|
|
2858
|
+
let cursor: string | undefined;
|
|
2859
|
+
let pageCount = 0;
|
|
2860
|
+
const pageSize = 3;
|
|
2861
|
+
|
|
2862
|
+
while (true) {
|
|
2863
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2864
|
+
first: pageSize,
|
|
2865
|
+
...(cursor && { after: cursor }),
|
|
2866
|
+
pagination: {
|
|
2867
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2868
|
+
term: 'Pattern',
|
|
2869
|
+
fields: ['label'],
|
|
2870
|
+
},
|
|
2871
|
+
});
|
|
2872
|
+
|
|
2873
|
+
pageCount++;
|
|
2874
|
+
allNames.push(
|
|
2875
|
+
...page.edges
|
|
2876
|
+
.map((e) => e.node.getField('name'))
|
|
2877
|
+
.filter((n): n is string => n !== null),
|
|
2878
|
+
);
|
|
2879
|
+
|
|
2880
|
+
if (!page.pageInfo.hasNextPage || pageCount > 10) {
|
|
2881
|
+
break;
|
|
2882
|
+
}
|
|
2883
|
+
cursor = page.pageInfo.endCursor!;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// With proper ORDER BY, we should get all matching records
|
|
2887
|
+
expect(allNames.length).toBe(20);
|
|
2888
|
+
|
|
2889
|
+
// Check that we got all unique names (no duplicates)
|
|
2890
|
+
const uniqueNames = new Set(allNames);
|
|
2891
|
+
expect(uniqueNames.size).toBe(20);
|
|
2892
|
+
|
|
2893
|
+
// Verify all expected names are present
|
|
2894
|
+
const sortedTestNames = [...testNames].sort();
|
|
2895
|
+
const sortedAllNames = [...allNames].sort();
|
|
2896
|
+
expect(sortedAllNames).toEqual(sortedTestNames);
|
|
2897
|
+
|
|
2898
|
+
// Test backward pagination
|
|
2899
|
+
const backwardNames: string[] = [];
|
|
2900
|
+
let backCursor: string | undefined;
|
|
2901
|
+
pageCount = 0;
|
|
2902
|
+
|
|
2903
|
+
while (true) {
|
|
2904
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2905
|
+
last: pageSize,
|
|
2906
|
+
...(backCursor && { before: backCursor }),
|
|
2907
|
+
pagination: {
|
|
2908
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
2909
|
+
term: 'Pattern',
|
|
2910
|
+
fields: ['label'],
|
|
2911
|
+
},
|
|
2912
|
+
});
|
|
2913
|
+
|
|
2914
|
+
pageCount++;
|
|
2915
|
+
backwardNames.unshift(
|
|
2916
|
+
...page.edges
|
|
2917
|
+
.map((e) => e.node.getField('name'))
|
|
2918
|
+
.filter((n): n is string => n !== null),
|
|
2919
|
+
);
|
|
2920
|
+
|
|
2921
|
+
if (!page.pageInfo.hasPreviousPage || pageCount > 10) {
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
backCursor = page.pageInfo.startCursor!;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// Backward pagination should also return all records
|
|
2928
|
+
expect(backwardNames.length).toBe(20);
|
|
2929
|
+
expect(new Set(backwardNames).size).toBe(20);
|
|
2930
|
+
|
|
2931
|
+
// With ORDER BY (search fields + ID), the ordering is deterministic
|
|
2932
|
+
// Forward and backward should produce the same order since we're ordering by name ASC/DESC + ID
|
|
2933
|
+
expect(allNames).toEqual(backwardNames);
|
|
2934
|
+
|
|
2935
|
+
// Verify the ordering follows the search fields (name in this case)
|
|
2936
|
+
// Since we order by name ASC for forward pagination, names should be sorted
|
|
2937
|
+
const expectedOrder = [...testNames].sort();
|
|
2938
|
+
expect(allNames).toEqual(expectedOrder);
|
|
2939
|
+
});
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
describe(PaginationStrategy.TRIGRAM_SEARCH, () => {
|
|
2943
|
+
it('supports trigram similarity search', async () => {
|
|
2944
|
+
const vc = new ViewerContext(
|
|
2945
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
2946
|
+
);
|
|
2947
|
+
|
|
2948
|
+
// Enable pg_trgm extension for trigram similarity
|
|
2949
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
2950
|
+
|
|
2951
|
+
// Create test data with similar names
|
|
2952
|
+
const names = ['Johnson', 'Jonson', 'Johnsen', 'Smith', 'Smyth', 'Schmidt'];
|
|
2953
|
+
for (let i = 0; i < names.length; i++) {
|
|
2954
|
+
await PostgresTestEntity.creator(vc)
|
|
2955
|
+
.setField('name', names[i]!)
|
|
2956
|
+
.setField('label', names[i]!)
|
|
2957
|
+
.setField('hasACat', i < 3) // First 3 have cats
|
|
2958
|
+
.createAsync();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// Search for similar names to "Johnson" using trigram
|
|
2962
|
+
const trigramSearch = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2963
|
+
first: 10,
|
|
2964
|
+
pagination: {
|
|
2965
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
2966
|
+
term: 'Johnson',
|
|
2967
|
+
fields: ['label'],
|
|
2968
|
+
threshold: 0.3, // Similarity threshold
|
|
2969
|
+
},
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
// Should find exact match and similar names, ordered by relevance
|
|
2973
|
+
expect(trigramSearch.edges.length).toBeGreaterThan(0);
|
|
2974
|
+
// Exact match should come first due to ILIKE matching
|
|
2975
|
+
expect(trigramSearch.edges[0]?.node.getField('name')).toBe('Johnson');
|
|
2976
|
+
|
|
2977
|
+
// The similar names (Jonson, Johnsen) should also be included
|
|
2978
|
+
const foundNames = trigramSearch.edges.map((e) => e.node.getField('name'));
|
|
2979
|
+
expect(foundNames).toContain('Jonson');
|
|
2980
|
+
expect(foundNames).toContain('Johnsen');
|
|
2981
|
+
|
|
2982
|
+
// Test combining with WHERE clause
|
|
2983
|
+
const filteredTrigram = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
2984
|
+
first: 10,
|
|
2985
|
+
where: sql`has_a_cat = ${true}`,
|
|
2986
|
+
pagination: {
|
|
2987
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
2988
|
+
term: 'Johnson',
|
|
2989
|
+
fields: ['label'],
|
|
2990
|
+
threshold: 0.3,
|
|
2991
|
+
},
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
// Only the Johnson-like names with cats
|
|
2995
|
+
expect(filteredTrigram.edges.length).toBeGreaterThan(0);
|
|
2996
|
+
expect(filteredTrigram.edges.length).toBeLessThanOrEqual(3);
|
|
2997
|
+
filteredTrigram.edges.forEach((edge) => {
|
|
2998
|
+
expect(edge.node.getField('hasACat')).toBe(true);
|
|
2999
|
+
});
|
|
3000
|
+
});
|
|
3001
|
+
|
|
3002
|
+
it('supports trigram search with cursor pagination', async () => {
|
|
3003
|
+
const vc = new ViewerContext(
|
|
3004
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3005
|
+
);
|
|
3006
|
+
|
|
3007
|
+
// Enable pg_trgm extension for trigram similarity
|
|
3008
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
3009
|
+
|
|
3010
|
+
// Create test data with similar names
|
|
3011
|
+
const names = [
|
|
3012
|
+
'Johnson',
|
|
3013
|
+
'Jonson',
|
|
3014
|
+
'Johnsen',
|
|
3015
|
+
'Johnston',
|
|
3016
|
+
'Johan',
|
|
3017
|
+
'Smith',
|
|
3018
|
+
'Smyth',
|
|
3019
|
+
'Schmidt',
|
|
3020
|
+
'Smithers',
|
|
3021
|
+
'Smythe',
|
|
3022
|
+
];
|
|
3023
|
+
for (let i = 0; i < names.length; i++) {
|
|
3024
|
+
await PostgresTestEntity.creator(vc)
|
|
3025
|
+
.setField('name', names[i]!)
|
|
3026
|
+
.setField('label', names[i]!)
|
|
3027
|
+
.setField('hasACat', i < 5) // First 5 have cats (Johnson-like names)
|
|
3028
|
+
.createAsync();
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// First page with trigram search (no cursor)
|
|
3032
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3033
|
+
first: 3,
|
|
3034
|
+
pagination: {
|
|
3035
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3036
|
+
term: 'Johnson',
|
|
3037
|
+
fields: ['label'],
|
|
3038
|
+
threshold: 0.3,
|
|
3039
|
+
},
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
// Should have results ordered by relevance
|
|
3043
|
+
expect(firstPage.edges.length).toBeGreaterThan(0);
|
|
3044
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Johnson'); // Exact match first
|
|
3045
|
+
const firstPageCursor = firstPage.pageInfo.endCursor;
|
|
3046
|
+
expect(firstPageCursor).not.toBeNull();
|
|
3047
|
+
|
|
3048
|
+
// Second page with cursor
|
|
3049
|
+
// Note: For trigram search with cursor, we use regular orderBy instead of custom order
|
|
3050
|
+
// so results might not be in perfect similarity order, but should still be filtered
|
|
3051
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3052
|
+
first: 3,
|
|
3053
|
+
after: firstPageCursor!,
|
|
3054
|
+
pagination: {
|
|
3055
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3056
|
+
term: 'Johnson',
|
|
3057
|
+
fields: ['label'],
|
|
3058
|
+
threshold: 0.3,
|
|
3059
|
+
},
|
|
3060
|
+
});
|
|
3061
|
+
|
|
3062
|
+
// Should have results (might be empty if first page had all results)
|
|
3063
|
+
expect(secondPage.edges.length).toBeGreaterThanOrEqual(0);
|
|
3064
|
+
|
|
3065
|
+
// The key test is that the query runs successfully with the searchOrderByClauses
|
|
3066
|
+
// being passed through the parallel query path
|
|
3067
|
+
|
|
3068
|
+
// Test backward pagination with cursor
|
|
3069
|
+
const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3070
|
+
last: 2,
|
|
3071
|
+
before: firstPageCursor!,
|
|
3072
|
+
pagination: {
|
|
3073
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3074
|
+
term: 'Johnson',
|
|
3075
|
+
fields: ['label'],
|
|
3076
|
+
threshold: 0.3,
|
|
3077
|
+
},
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
// Should have results before the cursor
|
|
3081
|
+
expect(lastPage.edges.length).toBeGreaterThanOrEqual(0);
|
|
3082
|
+
|
|
3083
|
+
// Test with WHERE clause, cursor, and search
|
|
3084
|
+
const firstEdgeCursor = firstPage.edges[0]?.cursor;
|
|
3085
|
+
expect(firstEdgeCursor).toBeDefined();
|
|
3086
|
+
const filteredWithCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3087
|
+
first: 2,
|
|
3088
|
+
after: firstEdgeCursor!,
|
|
3089
|
+
where: sql`has_a_cat = ${true}`,
|
|
3090
|
+
pagination: {
|
|
3091
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3092
|
+
term: 'Johnson',
|
|
3093
|
+
fields: ['label'],
|
|
3094
|
+
threshold: 0.3,
|
|
3095
|
+
},
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
// Should have filtered results with correct
|
|
3099
|
+
expect(filteredWithCursor.edges.length).toBeGreaterThanOrEqual(0);
|
|
3100
|
+
filteredWithCursor.edges.forEach((edge) => {
|
|
3101
|
+
expect(edge.node.getField('hasACat')).toBe(true);
|
|
3102
|
+
});
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
it('correctly orders trigram search results for forward and backward pagination', async () => {
|
|
3106
|
+
const vc = new ViewerContext(
|
|
3107
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3108
|
+
);
|
|
3109
|
+
|
|
3110
|
+
// Enable pg_trgm extension for trigram similarity
|
|
3111
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
3112
|
+
|
|
3113
|
+
// Create test data with similar names and unique IDs for stable ordering
|
|
3114
|
+
const testData = [
|
|
3115
|
+
{ name: 'Johnson', hasACat: true }, // Exact match, should be first
|
|
3116
|
+
{ name: 'Jonson', hasACat: true }, // High similarity
|
|
3117
|
+
{ name: 'Johnsen', hasACat: true }, // High similarity
|
|
3118
|
+
{ name: 'Johnston', hasACat: true }, // Medium similarity
|
|
3119
|
+
{ name: 'Johan', hasACat: true }, // Lower similarity
|
|
3120
|
+
{ name: 'John', hasACat: false }, // Lower similarity
|
|
3121
|
+
{ name: 'Smith', hasACat: false }, // No similarity
|
|
3122
|
+
{ name: 'Williams', hasACat: false }, // No similarity
|
|
3123
|
+
];
|
|
3124
|
+
|
|
3125
|
+
const createdEntities = [];
|
|
3126
|
+
for (const data of testData) {
|
|
3127
|
+
const entity = await PostgresTestEntity.creator(vc)
|
|
3128
|
+
.setField('name', data.name)
|
|
3129
|
+
.setField('label', data.name)
|
|
3130
|
+
.setField('hasACat', data.hasACat)
|
|
3131
|
+
.createAsync();
|
|
3132
|
+
createdEntities.push(entity);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// Test 1: Forward pagination (first)
|
|
3136
|
+
const firstPageForward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3137
|
+
first: 4,
|
|
3138
|
+
pagination: {
|
|
3139
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3140
|
+
term: 'Johnson',
|
|
3141
|
+
fields: ['label'],
|
|
3142
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3143
|
+
threshold: 0.2,
|
|
3144
|
+
},
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
// Johnson should be first (exact match), followed by high similarity matches
|
|
3148
|
+
expect(firstPageForward.edges.length).toBeGreaterThan(0);
|
|
3149
|
+
expect(firstPageForward.edges[0]?.node.getField('name')).toBe('Johnson');
|
|
3150
|
+
|
|
3151
|
+
// All results should match the search term
|
|
3152
|
+
const forwardNames = firstPageForward.edges.map((e) => e.node.getField('name'));
|
|
3153
|
+
expect(forwardNames).not.toContain('Smith');
|
|
3154
|
+
expect(forwardNames).not.toContain('Williams');
|
|
3155
|
+
|
|
3156
|
+
// Test 2: Backward pagination (last)
|
|
3157
|
+
const lastPageBackward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3158
|
+
last: 4,
|
|
3159
|
+
pagination: {
|
|
3160
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3161
|
+
term: 'Johnson',
|
|
3162
|
+
fields: ['label'],
|
|
3163
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3164
|
+
threshold: 0.2,
|
|
3165
|
+
},
|
|
3166
|
+
});
|
|
3167
|
+
|
|
3168
|
+
// Results should be in the same order (not reversed) after internal processing
|
|
3169
|
+
expect(lastPageBackward.edges.length).toBeGreaterThan(0);
|
|
3170
|
+
const backwardNames = lastPageBackward.edges.map((e) => e.node.getField('name'));
|
|
3171
|
+
|
|
3172
|
+
// Should not include non-matching names
|
|
3173
|
+
expect(backwardNames).not.toContain('Smith');
|
|
3174
|
+
expect(backwardNames).not.toContain('Williams');
|
|
3175
|
+
|
|
3176
|
+
// Test 3: Test cursor pagination with trigram search
|
|
3177
|
+
// With the improved implementation, TRIGRAM cursor pagination now preserves
|
|
3178
|
+
// similarity-based ordering by computing similarity scores dynamically via subquery
|
|
3179
|
+
const firstPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3180
|
+
first: 3,
|
|
3181
|
+
pagination: {
|
|
3182
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3183
|
+
term: 'Johnson',
|
|
3184
|
+
fields: ['label'],
|
|
3185
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3186
|
+
threshold: 0.2,
|
|
3187
|
+
},
|
|
3188
|
+
});
|
|
632
3189
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
)
|
|
3190
|
+
expect(firstPageForwardCursor.edges.length).toBeGreaterThan(0);
|
|
3191
|
+
const firstPageForwardCursorData = firstPageForwardCursor.edges.map((e) => ({
|
|
3192
|
+
name: e.node.getField('name'),
|
|
3193
|
+
id: e.node.getID(),
|
|
3194
|
+
createdAt: e.node.getField('createdAt'),
|
|
3195
|
+
}));
|
|
3196
|
+
const firstPageForwardCursorIDs = firstPageForwardCursorData.map((d) => d.id);
|
|
3197
|
+
|
|
3198
|
+
const secondPageForwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3199
|
+
first: 3,
|
|
3200
|
+
after: firstPageForwardCursor.pageInfo.endCursor!,
|
|
3201
|
+
pagination: {
|
|
3202
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3203
|
+
term: 'Johnson',
|
|
3204
|
+
fields: ['label'],
|
|
3205
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3206
|
+
threshold: 0.2,
|
|
3207
|
+
},
|
|
3208
|
+
});
|
|
639
3209
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
3210
|
+
const secondPageForwardCursorData = secondPageForwardCursor.edges.map((e) => ({
|
|
3211
|
+
name: e.node.getField('name'),
|
|
3212
|
+
id: e.node.getID(),
|
|
3213
|
+
createdAt: e.node.getField('createdAt'),
|
|
3214
|
+
}));
|
|
3215
|
+
const secondPageForwardCursorIDs = secondPageForwardCursorData.map((d) => d.id);
|
|
3216
|
+
expect(secondPageForwardCursorIDs.length).toBeGreaterThan(0);
|
|
3217
|
+
|
|
3218
|
+
// With the new subquery-based cursor implementation, there should be no overlap
|
|
3219
|
+
// between pages as ordering is perfectly preserved
|
|
3220
|
+
const overlapForwardCursor = firstPageForwardCursorIDs.filter((id) =>
|
|
3221
|
+
secondPageForwardCursorIDs.includes(id),
|
|
3222
|
+
);
|
|
3223
|
+
expect(overlapForwardCursor).toHaveLength(0);
|
|
3224
|
+
|
|
3225
|
+
// Test 4: test backward cursor pagination with trigram search
|
|
3226
|
+
const firstPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3227
|
+
last: 3,
|
|
3228
|
+
pagination: {
|
|
3229
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3230
|
+
term: 'Johnson',
|
|
3231
|
+
fields: ['label'],
|
|
3232
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3233
|
+
threshold: 0.2,
|
|
3234
|
+
},
|
|
3235
|
+
});
|
|
3236
|
+
|
|
3237
|
+
const firstPageBackwardCursorData = firstPageBackwardCursor.edges.map((e) => ({
|
|
3238
|
+
name: e.node.getField('name'),
|
|
3239
|
+
id: e.node.getID(),
|
|
3240
|
+
createdAt: e.node.getField('createdAt'),
|
|
3241
|
+
}));
|
|
3242
|
+
const firstPageBackwardIDs = firstPageBackwardCursorData.map((d) => d.id);
|
|
3243
|
+
expect(firstPageBackwardIDs.length).toBeGreaterThan(0);
|
|
3244
|
+
|
|
3245
|
+
const secondPageBackwardCursor = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3246
|
+
last: 3,
|
|
3247
|
+
before: firstPageBackwardCursor.pageInfo.startCursor!,
|
|
3248
|
+
pagination: {
|
|
3249
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3250
|
+
term: 'Johnson',
|
|
3251
|
+
fields: ['label'],
|
|
3252
|
+
extraOrderByFields: ['createdAt'], // Ensure stable ordering for similar scores since ID is uuidv4
|
|
3253
|
+
threshold: 0.2,
|
|
3254
|
+
},
|
|
3255
|
+
});
|
|
3256
|
+
|
|
3257
|
+
const secondPageBackwardCursorData = secondPageBackwardCursor.edges.map((e) => ({
|
|
3258
|
+
name: e.node.getField('name'),
|
|
3259
|
+
id: e.node.getID(),
|
|
3260
|
+
createdAt: e.node.getField('createdAt'),
|
|
3261
|
+
}));
|
|
3262
|
+
const secondPageBackwardIDs = secondPageBackwardCursorData.map((d) => d.id);
|
|
3263
|
+
expect(secondPageBackwardIDs.length).toBeGreaterThan(0);
|
|
3264
|
+
|
|
3265
|
+
// With the new subquery-based cursor implementation, there should be no overlap
|
|
3266
|
+
// between pages as ordering is perfectly preserved
|
|
3267
|
+
const overlapBackwardCursor = firstPageBackwardIDs.filter((id) =>
|
|
3268
|
+
secondPageBackwardIDs.includes(id),
|
|
3269
|
+
);
|
|
3270
|
+
expect(overlapBackwardCursor).toHaveLength(0);
|
|
646
3271
|
});
|
|
647
|
-
});
|
|
648
3272
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const vc1 = new ViewerContext(
|
|
3273
|
+
it('supports extraOrderByFields with TRIGRAM search for stable cursor pagination', async () => {
|
|
3274
|
+
const vc = new ViewerContext(
|
|
652
3275
|
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
653
3276
|
);
|
|
654
3277
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
3278
|
+
// Enable pg_trgm extension for trigram similarity
|
|
3279
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
3280
|
+
|
|
3281
|
+
// Create test data with similar names and different hasACat values
|
|
3282
|
+
const testData = [
|
|
3283
|
+
{ name: 'Johnson', hasACat: true }, // Exact match
|
|
3284
|
+
{ name: 'Jonson', hasACat: false }, // High similarity
|
|
3285
|
+
{ name: 'Johnsen', hasACat: true }, // High similarity
|
|
3286
|
+
{ name: 'Johnston', hasACat: false }, // Medium similarity
|
|
3287
|
+
{ name: 'Johan', hasACat: true }, // Lower similarity
|
|
3288
|
+
{ name: 'John', hasACat: false }, // Lower similarity
|
|
3289
|
+
{ name: 'Johnny', hasACat: true }, // Lower similarity
|
|
3290
|
+
{ name: 'Smith', hasACat: false }, // No match
|
|
3291
|
+
];
|
|
3292
|
+
|
|
3293
|
+
for (const data of testData) {
|
|
3294
|
+
await PostgresTestEntity.creator(vc)
|
|
3295
|
+
.setField('name', data.name)
|
|
3296
|
+
.setField('label', data.name)
|
|
3297
|
+
.setField('hasACat', data.hasACat)
|
|
3298
|
+
.createAsync();
|
|
3299
|
+
}
|
|
658
3300
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
3301
|
+
// Test TRIGRAM search with extraOrderByFields for stable pagination
|
|
3302
|
+
const firstPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3303
|
+
first: 3,
|
|
3304
|
+
pagination: {
|
|
3305
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3306
|
+
term: 'Johnson',
|
|
3307
|
+
fields: ['label'],
|
|
3308
|
+
threshold: 0.2,
|
|
3309
|
+
extraOrderByFields: ['createdAt'], // Add extra stable ordering
|
|
3310
|
+
},
|
|
3311
|
+
});
|
|
665
3312
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
3313
|
+
expect(firstPage.edges.length).toBeGreaterThan(0);
|
|
3314
|
+
expect(firstPage.edges[0]?.node.getField('name')).toBe('Johnson'); // Exact match first
|
|
3315
|
+
|
|
3316
|
+
const firstPageCursor = firstPage.pageInfo.endCursor;
|
|
3317
|
+
expect(firstPageCursor).not.toBeNull();
|
|
3318
|
+
|
|
3319
|
+
// Get second page using cursor
|
|
3320
|
+
// With extraOrderByFields, cursor includes hasACat field which provides more stable pagination
|
|
3321
|
+
const secondPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3322
|
+
first: 3,
|
|
3323
|
+
after: firstPageCursor!,
|
|
3324
|
+
pagination: {
|
|
3325
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3326
|
+
term: 'Johnson',
|
|
3327
|
+
fields: ['label'],
|
|
3328
|
+
threshold: 0.2,
|
|
3329
|
+
extraOrderByFields: ['createdAt'],
|
|
3330
|
+
},
|
|
3331
|
+
});
|
|
672
3332
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
).
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
).
|
|
3333
|
+
// Store first page names for comparison
|
|
3334
|
+
const firstPageNames = firstPage.edges.map((e) => e.node.getField('name'));
|
|
3335
|
+
const secondPageNames = secondPage.edges.map((e) => e.node.getField('name'));
|
|
3336
|
+
|
|
3337
|
+
// Verify no overlap between pages (ordering is preserved)
|
|
3338
|
+
const overlap = firstPageNames.filter((name) => secondPageNames.includes(name));
|
|
3339
|
+
expect(overlap).toHaveLength(0);
|
|
3340
|
+
|
|
3341
|
+
// Test backward pagination with extraOrderByFields
|
|
3342
|
+
const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3343
|
+
last: 2,
|
|
3344
|
+
pagination: {
|
|
3345
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3346
|
+
term: 'Johnson',
|
|
3347
|
+
fields: ['label'],
|
|
3348
|
+
threshold: 0.2,
|
|
3349
|
+
extraOrderByFields: ['createdAt'],
|
|
3350
|
+
},
|
|
3351
|
+
});
|
|
679
3352
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
3353
|
+
expect(lastPage.edges.length).toBeGreaterThan(0);
|
|
3354
|
+
|
|
3355
|
+
// Test that extraOrderByFields provides consistent ordering
|
|
3356
|
+
// Get all results in one go for comparison
|
|
3357
|
+
const allResultsPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3358
|
+
first: 10,
|
|
3359
|
+
pagination: {
|
|
3360
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3361
|
+
term: 'Johnson',
|
|
3362
|
+
fields: ['label'],
|
|
3363
|
+
threshold: 0.2,
|
|
3364
|
+
extraOrderByFields: ['createdAt'],
|
|
3365
|
+
},
|
|
3366
|
+
});
|
|
686
3367
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
3368
|
+
// Verify results are ordered by: exact match first, then similarity, then hasACat, then id
|
|
3369
|
+
const allNames = allResultsPage.edges.map((e) => ({
|
|
3370
|
+
name: e.node.getField('name'),
|
|
3371
|
+
hasACat: e.node.getField('hasACat'),
|
|
3372
|
+
}));
|
|
3373
|
+
|
|
3374
|
+
// Johnson (exact match) should be first
|
|
3375
|
+
expect(allNames[0]?.name).toBe('Johnson');
|
|
693
3376
|
});
|
|
694
3377
|
});
|
|
695
3378
|
|
|
696
|
-
describe('
|
|
697
|
-
it('
|
|
698
|
-
const
|
|
3379
|
+
describe('postgresTransform search field specification', () => {
|
|
3380
|
+
it('supports ILIKE search with postgresTransform on a nullable field', async () => {
|
|
3381
|
+
const vc = new ViewerContext(
|
|
699
3382
|
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
700
3383
|
);
|
|
701
3384
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
await expect(
|
|
719
|
-
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterDelete'),
|
|
720
|
-
).resolves.not.toBeNull();
|
|
721
|
-
});
|
|
722
|
-
});
|
|
723
|
-
describe('validation transaction behavior', () => {
|
|
724
|
-
describe('create', () => {
|
|
725
|
-
it('rolls back transaction when trigger throws ', async () => {
|
|
726
|
-
const vc1 = new ViewerContext(
|
|
727
|
-
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
728
|
-
);
|
|
3385
|
+
// Create test data with mix of null and non-null name fields
|
|
3386
|
+
const testData = [
|
|
3387
|
+
{ name: 'Alice Johnson', label: 'alice' },
|
|
3388
|
+
{ name: null, label: 'bob' },
|
|
3389
|
+
{ name: 'Charlie Johnson', label: 'charlie' },
|
|
3390
|
+
{ name: null, label: 'david' },
|
|
3391
|
+
{ name: 'Eve Thompson', label: 'eve' },
|
|
3392
|
+
];
|
|
3393
|
+
|
|
3394
|
+
for (const data of testData) {
|
|
3395
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', data.label);
|
|
3396
|
+
if (data.name !== null) {
|
|
3397
|
+
creator.setField('name', data.name);
|
|
3398
|
+
}
|
|
3399
|
+
await creator.createAsync();
|
|
3400
|
+
}
|
|
729
3401
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
3402
|
+
// Search using a nullable field with COALESCE transform
|
|
3403
|
+
const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3404
|
+
first: 10,
|
|
3405
|
+
pagination: {
|
|
3406
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
3407
|
+
term: 'Johnson',
|
|
3408
|
+
fields: [
|
|
3409
|
+
{
|
|
3410
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3411
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3412
|
+
},
|
|
3413
|
+
},
|
|
3414
|
+
],
|
|
3415
|
+
},
|
|
741
3416
|
});
|
|
3417
|
+
|
|
3418
|
+
// Only entities with non-null name containing 'Johnson' should match
|
|
3419
|
+
expect(searchResults.edges).toHaveLength(2);
|
|
3420
|
+
expect(searchResults.edges[0]?.node.getField('name')).toBe('Alice Johnson');
|
|
3421
|
+
expect(searchResults.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
|
|
742
3422
|
});
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
3423
|
+
|
|
3424
|
+
it('supports ILIKE search cursor pagination with postgresTransform', async () => {
|
|
3425
|
+
const vc = new ViewerContext(
|
|
3426
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3427
|
+
);
|
|
3428
|
+
|
|
3429
|
+
// Create enough test data to require multiple pages
|
|
3430
|
+
const names = [
|
|
3431
|
+
'Pattern_01',
|
|
3432
|
+
'Pattern_02',
|
|
3433
|
+
'Pattern_03',
|
|
3434
|
+
'Pattern_04',
|
|
3435
|
+
'Pattern_05',
|
|
3436
|
+
'Pattern_06',
|
|
3437
|
+
];
|
|
3438
|
+
|
|
3439
|
+
for (let i = 0; i < names.length; i++) {
|
|
3440
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', `label_${i}`);
|
|
3441
|
+
// Alternate between setting name and leaving it null
|
|
3442
|
+
if (i % 3 !== 0) {
|
|
3443
|
+
creator.setField('name', names[i]!);
|
|
3444
|
+
}
|
|
3445
|
+
await creator.createAsync();
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// Paginate through results using transform on nullable field
|
|
3449
|
+
const allNames: string[] = [];
|
|
3450
|
+
let cursor: string | undefined;
|
|
3451
|
+
|
|
3452
|
+
while (true) {
|
|
3453
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3454
|
+
first: 2,
|
|
3455
|
+
...(cursor && { after: cursor }),
|
|
3456
|
+
pagination: {
|
|
3457
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
3458
|
+
term: 'Pattern',
|
|
3459
|
+
fields: [
|
|
3460
|
+
{
|
|
3461
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3462
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3463
|
+
},
|
|
3464
|
+
},
|
|
3465
|
+
],
|
|
3466
|
+
},
|
|
3467
|
+
});
|
|
3468
|
+
|
|
3469
|
+
allNames.push(
|
|
3470
|
+
...page.edges
|
|
3471
|
+
.map((e) => e.node.getField('name'))
|
|
3472
|
+
.filter((n): n is string => n !== null),
|
|
747
3473
|
);
|
|
748
3474
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
3475
|
+
if (!page.pageInfo.hasNextPage) {
|
|
3476
|
+
break;
|
|
3477
|
+
}
|
|
3478
|
+
cursor = page.pageInfo.endCursor!;
|
|
3479
|
+
}
|
|
752
3480
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
).rejects.toThrow('name cannot have value beforeCreateAndUpdate');
|
|
758
|
-
await expect(
|
|
759
|
-
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
|
760
|
-
'name',
|
|
761
|
-
'beforeCreateAndUpdate',
|
|
762
|
-
),
|
|
763
|
-
).resolves.toBeNull();
|
|
764
|
-
});
|
|
3481
|
+
// Should find only the entities with non-null names matching 'Pattern'
|
|
3482
|
+
// Indices 1, 2, 4, 5 have names set (indices 0, 3 are null)
|
|
3483
|
+
expect(allNames).toHaveLength(4);
|
|
3484
|
+
expect(new Set(allNames).size).toBe(4); // No duplicates
|
|
765
3485
|
});
|
|
766
|
-
describe('delete', () => {
|
|
767
|
-
it('validation should not run on a delete mutation', async () => {
|
|
768
|
-
const vc1 = new ViewerContext(
|
|
769
|
-
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
770
|
-
);
|
|
771
3486
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
3487
|
+
it('supports ILIKE search with mix of plain fields and postgresTransform fields', async () => {
|
|
3488
|
+
const vc = new ViewerContext(
|
|
3489
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3490
|
+
);
|
|
3491
|
+
|
|
3492
|
+
const testData = [
|
|
3493
|
+
{ name: null, label: 'SearchTarget_Alpha' },
|
|
3494
|
+
{ name: 'SearchTarget_Beta', label: 'other' },
|
|
3495
|
+
{ name: null, label: 'unrelated' },
|
|
3496
|
+
{ name: 'SearchTarget_Gamma', label: 'SearchTarget_Gamma' },
|
|
3497
|
+
];
|
|
3498
|
+
|
|
3499
|
+
for (const data of testData) {
|
|
3500
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', data.label);
|
|
3501
|
+
if (data.name !== null) {
|
|
3502
|
+
creator.setField('name', data.name);
|
|
3503
|
+
}
|
|
3504
|
+
await creator.createAsync();
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// Search across both a non-nullable field (label) and a nullable field with transform (name)
|
|
3508
|
+
const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3509
|
+
first: 10,
|
|
3510
|
+
pagination: {
|
|
3511
|
+
strategy: PaginationStrategy.ILIKE_SEARCH,
|
|
3512
|
+
term: 'SearchTarget',
|
|
3513
|
+
fields: [
|
|
3514
|
+
'label',
|
|
3515
|
+
{
|
|
3516
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3517
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3518
|
+
},
|
|
3519
|
+
},
|
|
3520
|
+
],
|
|
3521
|
+
},
|
|
782
3522
|
});
|
|
3523
|
+
|
|
3524
|
+
// Should find:
|
|
3525
|
+
// - Entity with label='SearchTarget_Alpha' (matches on label, name is null)
|
|
3526
|
+
// - Entity with name='SearchTarget_Beta' (matches on name, label is 'other')
|
|
3527
|
+
// - Entity with name='SearchTarget_Gamma' and label='SearchTarget_Gamma' (matches on both)
|
|
3528
|
+
expect(searchResults.edges).toHaveLength(3);
|
|
3529
|
+
const foundLabels = searchResults.edges.map((e) => e.node.getField('label'));
|
|
3530
|
+
expect(foundLabels).toContain('SearchTarget_Alpha');
|
|
3531
|
+
expect(foundLabels).toContain('other');
|
|
3532
|
+
expect(foundLabels).toContain('SearchTarget_Gamma');
|
|
783
3533
|
});
|
|
784
|
-
});
|
|
785
|
-
});
|
|
786
3534
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
3535
|
+
it('supports TRIGRAM search with postgresTransform on fields', async () => {
|
|
3536
|
+
const vc = new ViewerContext(
|
|
3537
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3538
|
+
);
|
|
790
3539
|
|
|
791
|
-
|
|
792
|
-
let preCommitInnerCallCount = 0;
|
|
793
|
-
let postCommitCallCount = 0;
|
|
3540
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
794
3541
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
3542
|
+
const testData = [
|
|
3543
|
+
{ name: 'Johnson', label: 'a' },
|
|
3544
|
+
{ name: null, label: 'b' },
|
|
3545
|
+
{ name: 'Jonson', label: 'c' },
|
|
3546
|
+
{ name: null, label: 'd' },
|
|
3547
|
+
{ name: 'Johnsen', label: 'e' },
|
|
3548
|
+
];
|
|
3549
|
+
|
|
3550
|
+
for (const data of testData) {
|
|
3551
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', data.label);
|
|
3552
|
+
if (data.name !== null) {
|
|
3553
|
+
creator.setField('name', data.name);
|
|
3554
|
+
}
|
|
3555
|
+
await creator.createAsync();
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
const searchResults = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3559
|
+
first: 10,
|
|
3560
|
+
pagination: {
|
|
3561
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3562
|
+
term: 'Johnson',
|
|
3563
|
+
fields: [
|
|
3564
|
+
{
|
|
3565
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3566
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3567
|
+
},
|
|
3568
|
+
},
|
|
3569
|
+
],
|
|
3570
|
+
threshold: 0.3,
|
|
3571
|
+
},
|
|
798
3572
|
});
|
|
799
|
-
queryContext.appendPreCommitCallback(async () => {
|
|
800
|
-
preCommitCallCount++;
|
|
801
|
-
}, 0);
|
|
802
3573
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
3574
|
+
// Should find similar names, null-name entities have similarity 0 (below threshold)
|
|
3575
|
+
expect(searchResults.edges.length).toBeGreaterThan(0);
|
|
3576
|
+
expect(searchResults.edges[0]?.node.getField('name')).toBe('Johnson'); // Exact match first
|
|
3577
|
+
const foundNames = searchResults.edges.map((e) => e.node.getField('name'));
|
|
3578
|
+
expect(foundNames).toContain('Jonson');
|
|
3579
|
+
expect(foundNames).toContain('Johnsen');
|
|
3580
|
+
// Null-name entities should not appear
|
|
3581
|
+
foundNames.forEach((name) => {
|
|
3582
|
+
expect(name).not.toBeNull();
|
|
810
3583
|
});
|
|
3584
|
+
});
|
|
811
3585
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
3586
|
+
it('supports TRIGRAM search with postgresTransform on extraOrderByFields with cursor pagination', async () => {
|
|
3587
|
+
const vc = new ViewerContext(
|
|
3588
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3589
|
+
);
|
|
3590
|
+
|
|
3591
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
3592
|
+
|
|
3593
|
+
// Create test data where the extra order by field (name) is nullable
|
|
3594
|
+
const testData = [
|
|
3595
|
+
{ name: 'ZZZ', label: 'Johnson' },
|
|
3596
|
+
{ name: null, label: 'Jonson' },
|
|
3597
|
+
{ name: 'AAA', label: 'Johnsen' },
|
|
3598
|
+
{ name: null, label: 'Johnston' },
|
|
3599
|
+
{ name: 'MMM', label: 'Johan' },
|
|
3600
|
+
{ name: 'BBB', label: 'John' },
|
|
3601
|
+
];
|
|
3602
|
+
|
|
3603
|
+
for (const data of testData) {
|
|
3604
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', data.label);
|
|
3605
|
+
if (data.name !== null) {
|
|
3606
|
+
creator.setField('name', data.name);
|
|
3607
|
+
}
|
|
3608
|
+
await creator.createAsync();
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// Paginate through trigram results with postgresTransform on extraOrderByFields
|
|
3612
|
+
const allLabels: string[] = [];
|
|
3613
|
+
let cursor: string | undefined;
|
|
3614
|
+
|
|
3615
|
+
while (true) {
|
|
3616
|
+
const page = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3617
|
+
first: 2,
|
|
3618
|
+
...(cursor && { after: cursor }),
|
|
3619
|
+
pagination: {
|
|
3620
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3621
|
+
term: 'Johnson',
|
|
3622
|
+
fields: ['label'],
|
|
3623
|
+
threshold: 0.2,
|
|
3624
|
+
extraOrderByFields: [
|
|
3625
|
+
{
|
|
3626
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3627
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3628
|
+
},
|
|
3629
|
+
},
|
|
3630
|
+
],
|
|
3631
|
+
},
|
|
822
3632
|
});
|
|
823
|
-
|
|
3633
|
+
|
|
3634
|
+
allLabels.push(...page.edges.map((e) => e.node.getField('label')));
|
|
3635
|
+
|
|
3636
|
+
if (!page.pageInfo.hasNextPage) {
|
|
3637
|
+
break;
|
|
3638
|
+
}
|
|
3639
|
+
cursor = page.pageInfo.endCursor!;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// Should have paginated through all matching results with no duplicates
|
|
3643
|
+
expect(allLabels.length).toBeGreaterThan(0);
|
|
3644
|
+
expect(new Set(allLabels).size).toBe(allLabels.length);
|
|
824
3645
|
});
|
|
825
3646
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
});
|
|
831
|
-
queryContext.appendPreCommitCallback(async () => {
|
|
832
|
-
preCommitCallCount++;
|
|
833
|
-
throw Error('wat');
|
|
834
|
-
}, 0);
|
|
835
|
-
}),
|
|
836
|
-
).rejects.toThrow('wat');
|
|
3647
|
+
it('supports backward pagination with postgresTransform on extraOrderByFields', async () => {
|
|
3648
|
+
const vc = new ViewerContext(
|
|
3649
|
+
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
|
|
3650
|
+
);
|
|
837
3651
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
3652
|
+
await knexInstance.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
|
3653
|
+
|
|
3654
|
+
const testData = [
|
|
3655
|
+
{ name: 'ZZZ', label: 'Johnson' },
|
|
3656
|
+
{ name: null, label: 'Jonson' },
|
|
3657
|
+
{ name: 'AAA', label: 'Johnsen' },
|
|
3658
|
+
{ name: null, label: 'Johnston' },
|
|
3659
|
+
{ name: 'MMM', label: 'Johan' },
|
|
3660
|
+
];
|
|
3661
|
+
|
|
3662
|
+
for (const data of testData) {
|
|
3663
|
+
const creator = PostgresTestEntity.creator(vc).setField('label', data.label);
|
|
3664
|
+
if (data.name !== null) {
|
|
3665
|
+
creator.setField('name', data.name);
|
|
3666
|
+
}
|
|
3667
|
+
await creator.createAsync();
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
const paginationSpec: PaginationSpecification<PostgresTestEntityFields> = {
|
|
3671
|
+
strategy: PaginationStrategy.TRIGRAM_SEARCH,
|
|
3672
|
+
term: 'Johnson',
|
|
3673
|
+
fields: ['label'],
|
|
3674
|
+
threshold: 0.2,
|
|
3675
|
+
extraOrderByFields: [
|
|
3676
|
+
{
|
|
3677
|
+
fieldConstructor(getFragmentForFieldName) {
|
|
3678
|
+
return sql`COALESCE(${getFragmentForFieldName('name')}, '')`;
|
|
3679
|
+
},
|
|
3680
|
+
},
|
|
3681
|
+
],
|
|
3682
|
+
};
|
|
3683
|
+
|
|
3684
|
+
// Get all results forward
|
|
3685
|
+
const allForward = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3686
|
+
first: 10,
|
|
3687
|
+
pagination: paginationSpec,
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
// Get last 2 results backward
|
|
3691
|
+
const lastPage = await PostgresTestEntity.knexLoader(vc).loadPageAsync({
|
|
3692
|
+
last: 2,
|
|
3693
|
+
pagination: paginationSpec,
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
expect(lastPage.edges).toHaveLength(2);
|
|
3697
|
+
|
|
3698
|
+
// The last 2 from backward pagination should match the last 2 from forward pagination
|
|
3699
|
+
const forwardLastTwo = allForward.edges.slice(-2).map((e) => e.node.getID());
|
|
3700
|
+
const backwardResults = lastPage.edges.map((e) => e.node.getID());
|
|
3701
|
+
expect(backwardResults).toEqual(forwardLastTwo);
|
|
3702
|
+
});
|
|
841
3703
|
});
|
|
842
3704
|
});
|
|
843
3705
|
});
|