@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.
Files changed (91) hide show
  1. package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +278 -0
  2. package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
  3. package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
  4. package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
  5. package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
  6. package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
  7. package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
  8. package/build/src/BaseSQLQueryBuilder.js +87 -0
  9. package/build/src/BaseSQLQueryBuilder.js.map +1 -0
  10. package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
  11. package/build/src/EnforcingKnexEntityLoader.js +166 -0
  12. package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
  13. package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
  14. package/build/src/KnexEntityLoaderFactory.js +39 -0
  15. package/build/src/KnexEntityLoaderFactory.js.map +1 -0
  16. package/build/src/PaginationStrategy.d.ts +30 -0
  17. package/build/src/PaginationStrategy.js +35 -0
  18. package/build/src/PaginationStrategy.js.map +1 -0
  19. package/build/src/PostgresEntity.d.ts +25 -0
  20. package/build/src/PostgresEntity.js +39 -0
  21. package/build/src/PostgresEntity.js.map +1 -0
  22. package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
  23. package/build/src/PostgresEntityDatabaseAdapter.js +33 -11
  24. package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
  25. package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
  26. package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
  27. package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
  28. package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
  29. package/build/src/ReadonlyPostgresEntity.js +39 -0
  30. package/build/src/ReadonlyPostgresEntity.js.map +1 -0
  31. package/build/src/SQLOperator.d.ts +267 -0
  32. package/build/src/SQLOperator.js +474 -0
  33. package/build/src/SQLOperator.js.map +1 -0
  34. package/build/src/index.d.ts +15 -0
  35. package/build/src/index.js +15 -0
  36. package/build/src/index.js.map +1 -1
  37. package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
  38. package/build/src/internal/EntityKnexDataManager.js +453 -0
  39. package/build/src/internal/EntityKnexDataManager.js.map +1 -0
  40. package/build/src/internal/getKnexDataManager.d.ts +3 -0
  41. package/build/src/internal/getKnexDataManager.js +19 -0
  42. package/build/src/internal/getKnexDataManager.js.map +1 -0
  43. package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
  44. package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
  45. package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
  46. package/build/src/internal/utilityTypes.d.ts +5 -0
  47. package/build/src/internal/utilityTypes.js +5 -0
  48. package/build/src/internal/utilityTypes.js.map +1 -0
  49. package/build/src/internal/weakMaps.d.ts +9 -0
  50. package/build/src/internal/weakMaps.js +20 -0
  51. package/build/src/internal/weakMaps.js.map +1 -0
  52. package/build/src/knexLoader.d.ts +18 -0
  53. package/build/src/knexLoader.js +31 -0
  54. package/build/src/knexLoader.js.map +1 -0
  55. package/package.json +6 -5
  56. package/src/AuthorizationResultBasedKnexEntityLoader.ts +537 -0
  57. package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
  58. package/src/BaseSQLQueryBuilder.ts +114 -0
  59. package/src/EnforcingKnexEntityLoader.ts +271 -0
  60. package/src/KnexEntityLoaderFactory.ts +130 -0
  61. package/src/PaginationStrategy.ts +32 -0
  62. package/src/PostgresEntity.ts +118 -0
  63. package/src/PostgresEntityDatabaseAdapter.ts +81 -24
  64. package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
  65. package/src/ReadonlyPostgresEntity.ts +115 -0
  66. package/src/SQLOperator.ts +630 -0
  67. package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
  68. package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
  69. package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
  70. package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
  71. package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
  72. package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
  73. package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
  74. package/src/__tests__/EntityFields-test.ts +1 -1
  75. package/src/__tests__/PostgresEntity-test.ts +172 -0
  76. package/src/__tests__/ReadonlyEntity-test.ts +32 -0
  77. package/src/__tests__/SQLOperator-test.ts +871 -0
  78. package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
  79. package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
  80. package/src/__tests__/fixtures/TestEntity.ts +131 -0
  81. package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
  82. package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
  83. package/src/index.ts +15 -0
  84. package/src/internal/EntityKnexDataManager.ts +832 -0
  85. package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
  86. package/src/internal/__tests__/weakMaps-test.ts +25 -0
  87. package/src/internal/getKnexDataManager.ts +43 -0
  88. package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
  89. package/src/internal/utilityTypes.ts +11 -0
  90. package/src/internal/weakMaps.ts +19 -0
  91. package/src/knexLoader.ts +110 -0
@@ -0,0 +1,1167 @@
1
+ import {
2
+ EntityPrivacyPolicyEvaluationContext,
3
+ ViewerContext,
4
+ enforceResultsAsync,
5
+ IEntityMetricsAdapter,
6
+ EntityConstructionUtils,
7
+ EntityQueryContext,
8
+ } from '@expo/entity';
9
+ import { describe, expect, it } from '@jest/globals';
10
+ import { anyOfClass, anything, instance, mock, spy, verify, when } from 'ts-mockito';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+
13
+ import { AuthorizationResultBasedKnexEntityLoader } from '../AuthorizationResultBasedKnexEntityLoader';
14
+ import { OrderByOrdering } from '../BasePostgresEntityDatabaseAdapter';
15
+ import { PaginationStrategy } from '../PaginationStrategy';
16
+ import { sql } from '../SQLOperator';
17
+ import {
18
+ TestEntity,
19
+ testEntityConfiguration,
20
+ TestEntityPrivacyPolicy,
21
+ TestFields,
22
+ } from './fixtures/TestEntity';
23
+ import {
24
+ TestPaginationEntity,
25
+ testPaginationEntityConfiguration,
26
+ TestPaginationPrivacyPolicy,
27
+ TestPaginationFields,
28
+ } from './fixtures/TestPaginationEntity';
29
+ import { EntityKnexDataManager } from '../internal/EntityKnexDataManager';
30
+
31
+ describe(AuthorizationResultBasedKnexEntityLoader, () => {
32
+ it('loads entities with loadManyByFieldEqualityConjunction', async () => {
33
+ const privacyPolicy = new TestEntityPrivacyPolicy();
34
+ const spiedPrivacyPolicy = spy(privacyPolicy);
35
+ const viewerContext = instance(mock(ViewerContext));
36
+ const privacyPolicyEvaluationContext =
37
+ instance(
38
+ mock<
39
+ EntityPrivacyPolicyEvaluationContext<
40
+ TestFields,
41
+ 'customIdField',
42
+ ViewerContext,
43
+ TestEntity
44
+ >
45
+ >(),
46
+ );
47
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
48
+ const queryContext = instance(mock<EntityQueryContext>());
49
+
50
+ const knexDataManagerMock =
51
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
52
+
53
+ const id1 = uuidv4();
54
+ const id2 = uuidv4();
55
+ when(
56
+ knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
57
+ queryContext,
58
+ anything(),
59
+ anything(),
60
+ ),
61
+ ).thenResolve([
62
+ {
63
+ customIdField: id1,
64
+ stringField: 'huh',
65
+ intField: 4,
66
+ testIndexedField: '4',
67
+ dateField: new Date(),
68
+ nullableField: null,
69
+ },
70
+ {
71
+ customIdField: id2,
72
+ stringField: 'huh',
73
+ intField: 4,
74
+ testIndexedField: '5',
75
+ dateField: new Date(),
76
+ nullableField: null,
77
+ },
78
+ ]);
79
+
80
+ const constructionUtils = new EntityConstructionUtils(
81
+ viewerContext,
82
+ queryContext,
83
+ privacyPolicyEvaluationContext,
84
+ testEntityConfiguration,
85
+ TestEntity,
86
+ /* entitySelectedFields */ undefined,
87
+ privacyPolicy,
88
+ metricsAdapter,
89
+ );
90
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
91
+ queryContext,
92
+ instance(knexDataManagerMock),
93
+ metricsAdapter,
94
+ constructionUtils,
95
+ );
96
+ const entityResults = await enforceResultsAsync(
97
+ knexEntityLoader.loadManyByFieldEqualityConjunctionAsync([
98
+ {
99
+ fieldName: 'stringField',
100
+ fieldValue: 'huh',
101
+ },
102
+ {
103
+ fieldName: 'intField',
104
+ fieldValues: [4],
105
+ },
106
+ ]),
107
+ );
108
+ expect(entityResults).toHaveLength(2);
109
+ verify(
110
+ spiedPrivacyPolicy.authorizeReadAsync(
111
+ viewerContext,
112
+ queryContext,
113
+ privacyPolicyEvaluationContext,
114
+ anyOfClass(TestEntity),
115
+ anything(),
116
+ ),
117
+ ).twice();
118
+
119
+ await expect(
120
+ knexEntityLoader.loadManyByFieldEqualityConjunctionAsync([
121
+ { fieldName: 'customIdField', fieldValue: 'not-a-uuid' },
122
+ ]),
123
+ ).rejects.toThrow('Entity field not valid: TestEntity (customIdField = not-a-uuid)');
124
+ });
125
+
126
+ it('loads entities with loadFirstByFieldEqualityConjunction', async () => {
127
+ const privacyPolicy = new TestEntityPrivacyPolicy();
128
+ const spiedPrivacyPolicy = spy(privacyPolicy);
129
+ const viewerContext = instance(mock(ViewerContext));
130
+ const privacyPolicyEvaluationContext =
131
+ instance(
132
+ mock<
133
+ EntityPrivacyPolicyEvaluationContext<
134
+ TestFields,
135
+ 'customIdField',
136
+ ViewerContext,
137
+ TestEntity
138
+ >
139
+ >(),
140
+ );
141
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
142
+ const queryContext = instance(mock<EntityQueryContext>());
143
+
144
+ const knexDataManagerMock =
145
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
146
+ when(
147
+ knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
148
+ queryContext,
149
+ anything(),
150
+ anything(),
151
+ ),
152
+ ).thenResolve([
153
+ {
154
+ customIdField: 'id',
155
+ stringField: 'huh',
156
+ intField: 4,
157
+ testIndexedField: '5',
158
+ dateField: new Date(),
159
+ nullableField: null,
160
+ },
161
+ ]);
162
+
163
+ const constructionUtils = new EntityConstructionUtils(
164
+ viewerContext,
165
+ queryContext,
166
+ privacyPolicyEvaluationContext,
167
+ testEntityConfiguration,
168
+ TestEntity,
169
+ /* entitySelectedFields */ undefined,
170
+ privacyPolicy,
171
+ metricsAdapter,
172
+ );
173
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
174
+ queryContext,
175
+ instance(knexDataManagerMock),
176
+ metricsAdapter,
177
+ constructionUtils,
178
+ );
179
+ const result = await knexEntityLoader.loadFirstByFieldEqualityConjunctionAsync(
180
+ [
181
+ {
182
+ fieldName: 'stringField',
183
+ fieldValue: 'huh',
184
+ },
185
+ {
186
+ fieldName: 'intField',
187
+ fieldValue: 4,
188
+ },
189
+ ],
190
+ { orderBy: [{ fieldName: 'testIndexedField', order: OrderByOrdering.DESCENDING }] },
191
+ );
192
+ expect(result).not.toBeNull();
193
+ expect(result!.ok).toBe(true);
194
+
195
+ const resultEntity = result?.enforceValue();
196
+ expect(resultEntity).toBeInstanceOf(TestEntity);
197
+ expect(resultEntity!.getField('testIndexedField')).toEqual('5');
198
+
199
+ verify(
200
+ spiedPrivacyPolicy.authorizeReadAsync(
201
+ viewerContext,
202
+ queryContext,
203
+ privacyPolicyEvaluationContext,
204
+ anyOfClass(TestEntity),
205
+ anything(),
206
+ ),
207
+ ).once();
208
+ });
209
+
210
+ it('loads entities with loadManyByRawWhereClauseAsync', async () => {
211
+ const privacyPolicy = new TestEntityPrivacyPolicy();
212
+ const spiedPrivacyPolicy = spy(privacyPolicy);
213
+ const viewerContext = instance(mock(ViewerContext));
214
+ const privacyPolicyEvaluationContext =
215
+ instance(
216
+ mock<
217
+ EntityPrivacyPolicyEvaluationContext<
218
+ TestFields,
219
+ 'customIdField',
220
+ ViewerContext,
221
+ TestEntity
222
+ >
223
+ >(),
224
+ );
225
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
226
+ const queryContext = instance(mock<EntityQueryContext>());
227
+
228
+ const knexDataManagerMock =
229
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
230
+ when(
231
+ knexDataManagerMock.loadManyByRawWhereClauseAsync(
232
+ queryContext,
233
+ anything(),
234
+ anything(),
235
+ anything(),
236
+ ),
237
+ ).thenResolve([
238
+ {
239
+ customIdField: 'id',
240
+ stringField: 'huh',
241
+ intField: 4,
242
+ testIndexedField: '4',
243
+ dateField: new Date(),
244
+ nullableField: null,
245
+ },
246
+ ]);
247
+ const knexDataManager = instance(knexDataManagerMock);
248
+
249
+ const constructionUtils = new EntityConstructionUtils(
250
+ viewerContext,
251
+ queryContext,
252
+ privacyPolicyEvaluationContext,
253
+ testEntityConfiguration,
254
+ TestEntity,
255
+ /* entitySelectedFields */ undefined,
256
+ privacyPolicy,
257
+ metricsAdapter,
258
+ );
259
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
260
+ queryContext,
261
+ knexDataManager,
262
+ metricsAdapter,
263
+ constructionUtils,
264
+ );
265
+
266
+ const result = await knexEntityLoader.loadManyByRawWhereClauseAsync('id = ?', [1], {
267
+ orderBy: [{ fieldName: 'testIndexedField', order: OrderByOrdering.DESCENDING }],
268
+ });
269
+ expect(result).toHaveLength(1);
270
+ expect(result[0]).not.toBeNull();
271
+ expect(result[0]!.ok).toBe(true);
272
+ expect(result[0]!.enforceValue().getField('testIndexedField')).toEqual('4');
273
+
274
+ verify(
275
+ spiedPrivacyPolicy.authorizeReadAsync(
276
+ viewerContext,
277
+ queryContext,
278
+ privacyPolicyEvaluationContext,
279
+ anyOfClass(TestEntity),
280
+ anything(),
281
+ ),
282
+ ).once();
283
+ });
284
+
285
+ describe('loads entities with loadManyBySQL', () => {
286
+ it('returns entities with authorization results', async () => {
287
+ const privacyPolicy = new TestEntityPrivacyPolicy();
288
+ const spiedPrivacyPolicy = spy(privacyPolicy);
289
+ const viewerContext = instance(mock(ViewerContext));
290
+ const privacyPolicyEvaluationContext =
291
+ instance(
292
+ mock<
293
+ EntityPrivacyPolicyEvaluationContext<
294
+ TestFields,
295
+ 'customIdField',
296
+ ViewerContext,
297
+ TestEntity
298
+ >
299
+ >(),
300
+ );
301
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
302
+ const queryContext = instance(mock<EntityQueryContext>());
303
+
304
+ const knexDataManagerMock =
305
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
306
+
307
+ const id1 = uuidv4();
308
+ const id2 = uuidv4();
309
+ when(
310
+ knexDataManagerMock.loadManyBySQLFragmentAsync(queryContext, anything(), anything()),
311
+ ).thenResolve([
312
+ {
313
+ customIdField: id1,
314
+ stringField: 'test1',
315
+ intField: 1,
316
+ testIndexedField: '1',
317
+ dateField: new Date(),
318
+ nullableField: null,
319
+ },
320
+ {
321
+ customIdField: id2,
322
+ stringField: 'test2',
323
+ intField: 2,
324
+ testIndexedField: '2',
325
+ dateField: new Date(),
326
+ nullableField: null,
327
+ },
328
+ ]);
329
+
330
+ const constructionUtils = new EntityConstructionUtils(
331
+ viewerContext,
332
+ queryContext,
333
+ privacyPolicyEvaluationContext,
334
+ testEntityConfiguration,
335
+ TestEntity,
336
+ /* entitySelectedFields */ undefined,
337
+ privacyPolicy,
338
+ metricsAdapter,
339
+ );
340
+
341
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
342
+ queryContext,
343
+ instance(knexDataManagerMock),
344
+ metricsAdapter,
345
+ constructionUtils,
346
+ );
347
+
348
+ const queryBuilder = knexEntityLoader.loadManyBySQL(sql`intField > ${0}`);
349
+ const results = await queryBuilder.executeAsync();
350
+
351
+ expect(results).toHaveLength(2);
352
+ expect(results[0]!.ok).toBe(true);
353
+ expect(results[1]!.ok).toBe(true);
354
+
355
+ const entity1 = results[0]!.enforceValue();
356
+ const entity2 = results[1]!.enforceValue();
357
+ expect(entity1.getField('stringField')).toEqual('test1');
358
+ expect(entity2.getField('stringField')).toEqual('test2');
359
+
360
+ verify(
361
+ spiedPrivacyPolicy.authorizeReadAsync(
362
+ viewerContext,
363
+ queryContext,
364
+ privacyPolicyEvaluationContext,
365
+ anyOfClass(TestEntity),
366
+ anything(),
367
+ ),
368
+ ).twice();
369
+ });
370
+
371
+ it('supports chaining query builder methods', async () => {
372
+ const privacyPolicy = new TestEntityPrivacyPolicy();
373
+ const viewerContext = instance(mock(ViewerContext));
374
+ const privacyPolicyEvaluationContext =
375
+ instance(
376
+ mock<
377
+ EntityPrivacyPolicyEvaluationContext<
378
+ TestFields,
379
+ 'customIdField',
380
+ ViewerContext,
381
+ TestEntity
382
+ >
383
+ >(),
384
+ );
385
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
386
+ const queryContext = instance(mock<EntityQueryContext>());
387
+
388
+ const knexDataManagerMock =
389
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
390
+
391
+ when(
392
+ knexDataManagerMock.loadManyBySQLFragmentAsync(queryContext, anything(), anything()),
393
+ ).thenCall(async (_context, _fragment, modifiers) => {
394
+ // Verify the modifiers are passed correctly
395
+ expect(modifiers?.limit).toEqual(5);
396
+ expect(modifiers?.orderBy).toEqual([
397
+ { fieldName: 'intField', order: OrderByOrdering.DESCENDING },
398
+ ]);
399
+ return [
400
+ {
401
+ customIdField: uuidv4(),
402
+ stringField: 'result',
403
+ intField: 10,
404
+ testIndexedField: '1',
405
+ dateField: new Date(),
406
+ nullableField: null,
407
+ },
408
+ ];
409
+ });
410
+
411
+ const constructionUtils = new EntityConstructionUtils(
412
+ viewerContext,
413
+ queryContext,
414
+ privacyPolicyEvaluationContext,
415
+ testEntityConfiguration,
416
+ TestEntity,
417
+ /* entitySelectedFields */ undefined,
418
+ privacyPolicy,
419
+ metricsAdapter,
420
+ );
421
+
422
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
423
+ queryContext,
424
+ instance(knexDataManagerMock),
425
+ metricsAdapter,
426
+ constructionUtils,
427
+ );
428
+
429
+ const results = await knexEntityLoader
430
+ .loadManyBySQL(sql`status = ${'active'}`)
431
+ .orderBy('intField', OrderByOrdering.DESCENDING)
432
+ .limit(5)
433
+ .executeAsync();
434
+
435
+ expect(results).toHaveLength(1);
436
+ expect(results[0]!.ok).toBe(true);
437
+ });
438
+ });
439
+
440
+ describe('loads entities with loadPageAsync', () => {
441
+ it('returns paginated entities with forward pagination', async () => {
442
+ const privacyPolicy = new TestEntityPrivacyPolicy();
443
+ const spiedPrivacyPolicy = spy(privacyPolicy);
444
+ const viewerContext = instance(mock(ViewerContext));
445
+ const privacyPolicyEvaluationContext =
446
+ instance(
447
+ mock<
448
+ EntityPrivacyPolicyEvaluationContext<
449
+ TestFields,
450
+ 'customIdField',
451
+ ViewerContext,
452
+ TestEntity
453
+ >
454
+ >(),
455
+ );
456
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
457
+ const queryContext = instance(mock<EntityQueryContext>());
458
+
459
+ const knexDataManagerMock =
460
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
461
+
462
+ const id1 = uuidv4();
463
+ const id2 = uuidv4();
464
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
465
+ edges: [
466
+ {
467
+ cursor: 'cursor1',
468
+ node: {
469
+ customIdField: id1,
470
+ stringField: 'page1',
471
+ intField: 1,
472
+ testIndexedField: '1',
473
+ dateField: new Date(),
474
+ nullableField: null,
475
+ },
476
+ },
477
+ {
478
+ cursor: 'cursor2',
479
+ node: {
480
+ customIdField: id2,
481
+ stringField: 'page2',
482
+ intField: 2,
483
+ testIndexedField: '2',
484
+ dateField: new Date(),
485
+ nullableField: null,
486
+ },
487
+ },
488
+ ],
489
+ pageInfo: {
490
+ hasNextPage: true,
491
+ hasPreviousPage: false,
492
+ startCursor: 'cursor1',
493
+ endCursor: 'cursor2',
494
+ },
495
+ });
496
+
497
+ const constructionUtils = new EntityConstructionUtils(
498
+ viewerContext,
499
+ queryContext,
500
+ privacyPolicyEvaluationContext,
501
+ testEntityConfiguration,
502
+ TestEntity,
503
+ /* entitySelectedFields */ undefined,
504
+ privacyPolicy,
505
+ metricsAdapter,
506
+ );
507
+
508
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
509
+ queryContext,
510
+ instance(knexDataManagerMock),
511
+ metricsAdapter,
512
+ constructionUtils,
513
+ );
514
+
515
+ const connection = await knexEntityLoader.loadPageAsync({
516
+ first: 10,
517
+ where: sql`intField > ${0}`,
518
+ pagination: {
519
+ strategy: PaginationStrategy.STANDARD,
520
+ orderBy: [{ fieldName: 'intField', order: OrderByOrdering.ASCENDING }],
521
+ },
522
+ });
523
+
524
+ expect(connection.edges).toHaveLength(2);
525
+ expect(connection.edges[0]!.cursor).toEqual('cursor1');
526
+ expect(connection.edges[0]!.node.getField('stringField')).toEqual('page1');
527
+ expect(connection.edges[1]!.cursor).toEqual('cursor2');
528
+ expect(connection.edges[1]!.node.getField('stringField')).toEqual('page2');
529
+
530
+ expect(connection.pageInfo).toEqual({
531
+ hasNextPage: true,
532
+ hasPreviousPage: false,
533
+ startCursor: 'cursor1',
534
+ endCursor: 'cursor2',
535
+ });
536
+
537
+ verify(
538
+ spiedPrivacyPolicy.authorizeReadAsync(
539
+ viewerContext,
540
+ queryContext,
541
+ privacyPolicyEvaluationContext,
542
+ anyOfClass(TestEntity),
543
+ anything(),
544
+ ),
545
+ ).twice();
546
+ });
547
+
548
+ it('filters out entities that fail authorization', async () => {
549
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
550
+ const viewerContext = instance(mock(ViewerContext));
551
+ const privacyPolicyEvaluationContext =
552
+ instance(
553
+ mock<
554
+ EntityPrivacyPolicyEvaluationContext<
555
+ TestPaginationFields,
556
+ 'id',
557
+ ViewerContext,
558
+ TestPaginationEntity
559
+ >
560
+ >(),
561
+ );
562
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
563
+ const queryContext = instance(mock<EntityQueryContext>());
564
+
565
+ const knexDataManagerMock =
566
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
567
+
568
+ const id1 = uuidv4();
569
+ const id2 = uuidv4();
570
+ const id3 = uuidv4();
571
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
572
+ edges: [
573
+ {
574
+ cursor: 'cursor1',
575
+ node: {
576
+ id: id1,
577
+ name: 'Entity 1',
578
+ status: 'active',
579
+ createdAt: new Date('2024-01-01'),
580
+ score: 100,
581
+ },
582
+ },
583
+ {
584
+ cursor: 'cursor2',
585
+ node: {
586
+ id: id2,
587
+ name: 'Entity 2',
588
+ status: 'unauthorized', // This will fail authorization
589
+ createdAt: new Date('2024-01-02'),
590
+ score: 200,
591
+ },
592
+ },
593
+ {
594
+ cursor: 'cursor3',
595
+ node: {
596
+ id: id3,
597
+ name: 'Entity 3',
598
+ status: 'active',
599
+ createdAt: new Date('2024-01-03'),
600
+ score: 300,
601
+ },
602
+ },
603
+ ],
604
+ pageInfo: {
605
+ hasNextPage: false,
606
+ hasPreviousPage: false,
607
+ startCursor: 'cursor1',
608
+ endCursor: 'cursor3',
609
+ },
610
+ });
611
+
612
+ const constructionUtils = new EntityConstructionUtils(
613
+ viewerContext,
614
+ queryContext,
615
+ privacyPolicyEvaluationContext,
616
+ testPaginationEntityConfiguration,
617
+ TestPaginationEntity,
618
+ /* entitySelectedFields */ undefined,
619
+ privacyPolicy,
620
+ metricsAdapter,
621
+ );
622
+
623
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
624
+ queryContext,
625
+ instance(knexDataManagerMock),
626
+ metricsAdapter,
627
+ constructionUtils,
628
+ );
629
+
630
+ const connection = await knexEntityLoader.loadPageAsync({
631
+ first: 10,
632
+ where: sql`score > ${0}`,
633
+ pagination: {
634
+ strategy: PaginationStrategy.STANDARD,
635
+ orderBy: [{ fieldName: 'createdAt', order: OrderByOrdering.ASCENDING }],
636
+ },
637
+ });
638
+
639
+ // Should only have 2 entities (unauthorized one filtered out)
640
+ expect(connection.edges).toHaveLength(2);
641
+ expect(connection.edges[0]!.node.getField('name')).toEqual('Entity 1');
642
+ expect(connection.edges[1]!.node.getField('name')).toEqual('Entity 3');
643
+
644
+ // Cursors should be maintained from successful entities only
645
+ expect(connection.edges[0]!.cursor).toEqual('cursor1');
646
+ expect(connection.edges[1]!.cursor).toEqual('cursor3');
647
+
648
+ expect(connection.pageInfo).toEqual({
649
+ hasNextPage: false,
650
+ hasPreviousPage: false,
651
+ startCursor: 'cursor1',
652
+ endCursor: 'cursor3',
653
+ });
654
+ });
655
+
656
+ it('supports backward pagination with last/before', async () => {
657
+ const privacyPolicy = new TestEntityPrivacyPolicy();
658
+ const viewerContext = instance(mock(ViewerContext));
659
+ const privacyPolicyEvaluationContext =
660
+ instance(
661
+ mock<
662
+ EntityPrivacyPolicyEvaluationContext<
663
+ TestFields,
664
+ 'customIdField',
665
+ ViewerContext,
666
+ TestEntity
667
+ >
668
+ >(),
669
+ );
670
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
671
+ const queryContext = instance(mock<EntityQueryContext>());
672
+
673
+ const knexDataManagerMock =
674
+ mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
675
+
676
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
677
+ edges: [
678
+ {
679
+ cursor: 'cursor5',
680
+ node: {
681
+ customIdField: uuidv4(),
682
+ stringField: 'item5',
683
+ intField: 5,
684
+ testIndexedField: '5',
685
+ dateField: new Date(),
686
+ nullableField: null,
687
+ },
688
+ },
689
+ ],
690
+ pageInfo: {
691
+ hasNextPage: false,
692
+ hasPreviousPage: true,
693
+ startCursor: 'cursor5',
694
+ endCursor: 'cursor5',
695
+ },
696
+ });
697
+
698
+ const constructionUtils = new EntityConstructionUtils(
699
+ viewerContext,
700
+ queryContext,
701
+ privacyPolicyEvaluationContext,
702
+ testEntityConfiguration,
703
+ TestEntity,
704
+ /* entitySelectedFields */ undefined,
705
+ privacyPolicy,
706
+ metricsAdapter,
707
+ );
708
+
709
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
710
+ queryContext,
711
+ instance(knexDataManagerMock),
712
+ metricsAdapter,
713
+ constructionUtils,
714
+ );
715
+
716
+ const connection = await knexEntityLoader.loadPageAsync({
717
+ last: 5,
718
+ before: 'someCursor',
719
+ pagination: {
720
+ strategy: PaginationStrategy.STANDARD,
721
+ orderBy: [{ fieldName: 'intField', order: OrderByOrdering.ASCENDING }],
722
+ },
723
+ });
724
+
725
+ expect(connection.edges).toHaveLength(1);
726
+ expect(connection.pageInfo.hasPreviousPage).toBe(true);
727
+ expect(connection.pageInfo.hasNextPage).toBe(false);
728
+ });
729
+ });
730
+
731
+ describe('loads entities with loadPageAsync (search)', () => {
732
+ it('performs ILIKE search and filters unauthorized entities', async () => {
733
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
734
+ const viewerContext = instance(mock(ViewerContext));
735
+ const privacyPolicyEvaluationContext =
736
+ instance(
737
+ mock<
738
+ EntityPrivacyPolicyEvaluationContext<
739
+ TestPaginationFields,
740
+ 'id',
741
+ ViewerContext,
742
+ TestPaginationEntity
743
+ >
744
+ >(),
745
+ );
746
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
747
+ const queryContext = instance(mock<EntityQueryContext>());
748
+
749
+ const knexDataManagerMock =
750
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
751
+
752
+ const id1 = uuidv4();
753
+ const id2 = uuidv4();
754
+ const id3 = uuidv4();
755
+
756
+ // Mock data manager to return 3 entities from search
757
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
758
+ edges: [
759
+ {
760
+ cursor: 'cursor1',
761
+ node: {
762
+ id: id1,
763
+ name: 'Alice Johnson',
764
+ status: 'active',
765
+ createdAt: new Date(),
766
+ score: 1,
767
+ },
768
+ },
769
+ {
770
+ cursor: 'cursor2',
771
+ node: {
772
+ id: id2,
773
+ name: 'Bob Johnson',
774
+ status: 'unauthorized', // This will fail authorization per TestPaginationPrivacyPolicy
775
+ createdAt: new Date(),
776
+ score: 2,
777
+ },
778
+ },
779
+ {
780
+ cursor: 'cursor3',
781
+ node: {
782
+ id: id3,
783
+ name: 'Charlie Johnson',
784
+ status: 'active',
785
+ createdAt: new Date(),
786
+ score: 3,
787
+ },
788
+ },
789
+ ],
790
+ pageInfo: {
791
+ hasNextPage: false,
792
+ hasPreviousPage: false,
793
+ startCursor: 'cursor1',
794
+ endCursor: 'cursor3',
795
+ },
796
+ });
797
+
798
+ const constructionUtils = new EntityConstructionUtils(
799
+ viewerContext,
800
+ queryContext,
801
+ privacyPolicyEvaluationContext,
802
+ testPaginationEntityConfiguration,
803
+ TestPaginationEntity,
804
+ /* entitySelectedFields */ undefined,
805
+ privacyPolicy,
806
+ metricsAdapter,
807
+ );
808
+
809
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
810
+ queryContext,
811
+ instance(knexDataManagerMock),
812
+ metricsAdapter,
813
+ constructionUtils,
814
+ );
815
+
816
+ const connection = await knexEntityLoader.loadPageAsync({
817
+ first: 10,
818
+ pagination: {
819
+ strategy: PaginationStrategy.ILIKE_SEARCH,
820
+ term: 'Johnson',
821
+ fields: ['name'],
822
+ },
823
+ });
824
+
825
+ // Should only have 2 edges (Bob was filtered out)
826
+ expect(connection.edges).toHaveLength(2);
827
+ expect(connection.edges[0]?.node.getField('id')).toBe(id1);
828
+ expect(connection.edges[0]?.node.getField('name')).toBe('Alice Johnson');
829
+ expect(connection.edges[1]?.node.getField('id')).toBe(id3);
830
+ expect(connection.edges[1]?.node.getField('name')).toBe('Charlie Johnson');
831
+
832
+ // Cursors should be updated to reflect only authorized entities
833
+ expect(connection.pageInfo.startCursor).toBe('cursor1');
834
+ expect(connection.pageInfo.endCursor).toBe('cursor3');
835
+ });
836
+
837
+ it('performs TRIGRAM search with cursor pagination', async () => {
838
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
839
+ const viewerContext = instance(mock(ViewerContext));
840
+ const privacyPolicyEvaluationContext =
841
+ instance(
842
+ mock<
843
+ EntityPrivacyPolicyEvaluationContext<
844
+ TestPaginationFields,
845
+ 'id',
846
+ ViewerContext,
847
+ TestPaginationEntity
848
+ >
849
+ >(),
850
+ );
851
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
852
+ const queryContext = instance(mock<EntityQueryContext>());
853
+
854
+ const knexDataManagerMock =
855
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
856
+
857
+ const id1 = uuidv4();
858
+ const id2 = uuidv4();
859
+
860
+ // Mock first page of TRIGRAM search results
861
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
862
+ edges: [
863
+ {
864
+ cursor: 'cursor1',
865
+ node: {
866
+ id: id1,
867
+ name: 'Johnson', // Exact match
868
+ status: 'active',
869
+ createdAt: new Date(),
870
+ score: 1,
871
+ },
872
+ },
873
+ {
874
+ cursor: 'cursor2',
875
+ node: {
876
+ id: id2,
877
+ name: 'Jonson', // Similar match
878
+ status: 'active',
879
+ createdAt: new Date(),
880
+ score: 2,
881
+ },
882
+ },
883
+ ],
884
+ pageInfo: {
885
+ hasNextPage: true,
886
+ hasPreviousPage: false,
887
+ startCursor: 'cursor1',
888
+ endCursor: 'cursor2',
889
+ },
890
+ });
891
+
892
+ const constructionUtils = new EntityConstructionUtils(
893
+ viewerContext,
894
+ queryContext,
895
+ privacyPolicyEvaluationContext,
896
+ testPaginationEntityConfiguration,
897
+ TestPaginationEntity,
898
+ /* entitySelectedFields */ undefined,
899
+ privacyPolicy,
900
+ metricsAdapter,
901
+ );
902
+
903
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
904
+ queryContext,
905
+ instance(knexDataManagerMock),
906
+ metricsAdapter,
907
+ constructionUtils,
908
+ );
909
+
910
+ const connection = await knexEntityLoader.loadPageAsync({
911
+ first: 2,
912
+ after: 'someCursor',
913
+ pagination: {
914
+ strategy: PaginationStrategy.TRIGRAM_SEARCH,
915
+ term: 'Johnson',
916
+ fields: ['name'],
917
+ threshold: 0.3,
918
+ extraOrderByFields: ['score'],
919
+ },
920
+ });
921
+
922
+ expect(connection.edges).toHaveLength(2);
923
+ expect(connection.edges[0]?.node.getField('name')).toBe('Johnson');
924
+ expect(connection.edges[1]?.node.getField('name')).toBe('Jonson');
925
+ expect(connection.pageInfo.hasNextPage).toBe(true);
926
+ });
927
+
928
+ it('handles backward pagination with search', async () => {
929
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
930
+ const viewerContext = instance(mock(ViewerContext));
931
+ const privacyPolicyEvaluationContext =
932
+ instance(
933
+ mock<
934
+ EntityPrivacyPolicyEvaluationContext<
935
+ TestPaginationFields,
936
+ 'id',
937
+ ViewerContext,
938
+ TestPaginationEntity
939
+ >
940
+ >(),
941
+ );
942
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
943
+ const queryContext = instance(mock<EntityQueryContext>());
944
+
945
+ const knexDataManagerMock =
946
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
947
+
948
+ const id1 = uuidv4();
949
+ const id2 = uuidv4();
950
+
951
+ // Mock backward pagination results
952
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
953
+ edges: [
954
+ {
955
+ cursor: 'cursor1',
956
+ node: {
957
+ id: id1,
958
+ name: 'Charlie Smith',
959
+ status: 'active',
960
+ createdAt: new Date(),
961
+ score: 3,
962
+ },
963
+ },
964
+ {
965
+ cursor: 'cursor2',
966
+ node: {
967
+ id: id2,
968
+ name: 'David Smith',
969
+ status: 'active',
970
+ createdAt: new Date(),
971
+ score: 4,
972
+ },
973
+ },
974
+ ],
975
+ pageInfo: {
976
+ hasNextPage: false,
977
+ hasPreviousPage: true,
978
+ startCursor: 'cursor1',
979
+ endCursor: 'cursor2',
980
+ },
981
+ });
982
+
983
+ const constructionUtils = new EntityConstructionUtils(
984
+ viewerContext,
985
+ queryContext,
986
+ privacyPolicyEvaluationContext,
987
+ testPaginationEntityConfiguration,
988
+ TestPaginationEntity,
989
+ /* entitySelectedFields */ undefined,
990
+ privacyPolicy,
991
+ metricsAdapter,
992
+ );
993
+
994
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
995
+ queryContext,
996
+ instance(knexDataManagerMock),
997
+ metricsAdapter,
998
+ constructionUtils,
999
+ );
1000
+
1001
+ const connection = await knexEntityLoader.loadPageAsync({
1002
+ last: 2,
1003
+ before: 'someCursor',
1004
+ pagination: {
1005
+ strategy: PaginationStrategy.ILIKE_SEARCH,
1006
+ term: 'Smith',
1007
+ fields: ['name'],
1008
+ },
1009
+ });
1010
+
1011
+ expect(connection.edges).toHaveLength(2);
1012
+ expect(connection.pageInfo.hasPreviousPage).toBe(true);
1013
+ expect(connection.pageInfo.hasNextPage).toBe(false);
1014
+ });
1015
+
1016
+ it('handles empty search results', async () => {
1017
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
1018
+ const viewerContext = instance(mock(ViewerContext));
1019
+ const privacyPolicyEvaluationContext =
1020
+ instance(
1021
+ mock<
1022
+ EntityPrivacyPolicyEvaluationContext<
1023
+ TestPaginationFields,
1024
+ 'id',
1025
+ ViewerContext,
1026
+ TestPaginationEntity
1027
+ >
1028
+ >(),
1029
+ );
1030
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
1031
+ const queryContext = instance(mock<EntityQueryContext>());
1032
+
1033
+ const knexDataManagerMock =
1034
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
1035
+
1036
+ // Mock empty search results
1037
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
1038
+ edges: [],
1039
+ pageInfo: {
1040
+ hasNextPage: false,
1041
+ hasPreviousPage: false,
1042
+ startCursor: null,
1043
+ endCursor: null,
1044
+ },
1045
+ });
1046
+
1047
+ const constructionUtils = new EntityConstructionUtils(
1048
+ viewerContext,
1049
+ queryContext,
1050
+ privacyPolicyEvaluationContext,
1051
+ testPaginationEntityConfiguration,
1052
+ TestPaginationEntity,
1053
+ /* entitySelectedFields */ undefined,
1054
+ privacyPolicy,
1055
+ metricsAdapter,
1056
+ );
1057
+
1058
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
1059
+ queryContext,
1060
+ instance(knexDataManagerMock),
1061
+ metricsAdapter,
1062
+ constructionUtils,
1063
+ );
1064
+
1065
+ const connection = await knexEntityLoader.loadPageAsync({
1066
+ first: 10,
1067
+ pagination: {
1068
+ strategy: PaginationStrategy.ILIKE_SEARCH,
1069
+ term: 'NonexistentTerm',
1070
+ fields: ['name'],
1071
+ },
1072
+ });
1073
+
1074
+ expect(connection.edges).toHaveLength(0);
1075
+ expect(connection.pageInfo.startCursor).toBeNull();
1076
+ expect(connection.pageInfo.endCursor).toBeNull();
1077
+ });
1078
+
1079
+ it('handles all entities failing authorization', async () => {
1080
+ const privacyPolicy = new TestPaginationPrivacyPolicy();
1081
+ const viewerContext = instance(mock(ViewerContext));
1082
+ const privacyPolicyEvaluationContext =
1083
+ instance(
1084
+ mock<
1085
+ EntityPrivacyPolicyEvaluationContext<
1086
+ TestPaginationFields,
1087
+ 'id',
1088
+ ViewerContext,
1089
+ TestPaginationEntity
1090
+ >
1091
+ >(),
1092
+ );
1093
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
1094
+ const queryContext = instance(mock<EntityQueryContext>());
1095
+
1096
+ const knexDataManagerMock =
1097
+ mock<EntityKnexDataManager<TestPaginationFields, 'id'>>(EntityKnexDataManager);
1098
+
1099
+ const id1 = uuidv4();
1100
+ const id2 = uuidv4();
1101
+
1102
+ // Mock search results
1103
+ when(knexDataManagerMock.loadPageAsync(queryContext, anything())).thenResolve({
1104
+ edges: [
1105
+ {
1106
+ cursor: 'cursor1',
1107
+ node: {
1108
+ id: id1,
1109
+ name: 'Alice',
1110
+ status: 'unauthorized', // This will fail authorization
1111
+ createdAt: new Date(),
1112
+ score: 1,
1113
+ },
1114
+ },
1115
+ {
1116
+ cursor: 'cursor2',
1117
+ node: {
1118
+ id: id2,
1119
+ name: 'Bob',
1120
+ status: 'unauthorized', // This will fail authorization
1121
+ createdAt: new Date(),
1122
+ score: 2,
1123
+ },
1124
+ },
1125
+ ],
1126
+ pageInfo: {
1127
+ hasNextPage: false,
1128
+ hasPreviousPage: false,
1129
+ startCursor: 'cursor1',
1130
+ endCursor: 'cursor2',
1131
+ },
1132
+ });
1133
+
1134
+ const constructionUtils = new EntityConstructionUtils(
1135
+ viewerContext,
1136
+ queryContext,
1137
+ privacyPolicyEvaluationContext,
1138
+ testPaginationEntityConfiguration,
1139
+ TestPaginationEntity,
1140
+ /* entitySelectedFields */ undefined,
1141
+ privacyPolicy,
1142
+ metricsAdapter,
1143
+ );
1144
+
1145
+ const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
1146
+ queryContext,
1147
+ instance(knexDataManagerMock),
1148
+ metricsAdapter,
1149
+ constructionUtils,
1150
+ );
1151
+
1152
+ const connection = await knexEntityLoader.loadPageAsync({
1153
+ first: 10,
1154
+ pagination: {
1155
+ strategy: PaginationStrategy.ILIKE_SEARCH,
1156
+ term: 'test',
1157
+ fields: ['name'],
1158
+ },
1159
+ });
1160
+
1161
+ // All entities filtered out due to failed authorization
1162
+ expect(connection.edges).toHaveLength(0);
1163
+ expect(connection.pageInfo.startCursor).toBeNull();
1164
+ expect(connection.pageInfo.endCursor).toBeNull();
1165
+ });
1166
+ });
1167
+ });