@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
|
@@ -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
|
+
});
|