@expo/entity-database-adapter-knex 0.54.0 → 0.57.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 +279 -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 +32 -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 +261 -0
- package/build/src/SQLOperator.js +464 -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 +538 -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 +78 -24
- package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
- package/src/ReadonlyPostgresEntity.ts +115 -0
- package/src/SQLOperator.ts +603 -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 +831 -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,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityMetricsLoadType,
|
|
3
|
+
EntityQueryContext,
|
|
4
|
+
IEntityMetricsAdapter,
|
|
5
|
+
NoOpEntityMetricsAdapter,
|
|
6
|
+
} from '@expo/entity';
|
|
7
|
+
import { StubQueryContextProvider } from '@expo/entity-testing-utils';
|
|
8
|
+
import { describe, expect, it } from '@jest/globals';
|
|
9
|
+
import {
|
|
10
|
+
anyNumber,
|
|
11
|
+
anyString,
|
|
12
|
+
anything,
|
|
13
|
+
deepEqual,
|
|
14
|
+
instance,
|
|
15
|
+
mock,
|
|
16
|
+
resetCalls,
|
|
17
|
+
verify,
|
|
18
|
+
when,
|
|
19
|
+
} from 'ts-mockito';
|
|
20
|
+
|
|
21
|
+
import { OrderByOrdering } from '../../BasePostgresEntityDatabaseAdapter';
|
|
22
|
+
import { PaginationStrategy } from '../../PaginationStrategy';
|
|
23
|
+
import { PostgresEntityDatabaseAdapter } from '../../PostgresEntityDatabaseAdapter';
|
|
24
|
+
import {
|
|
25
|
+
TestEntity,
|
|
26
|
+
TestFields,
|
|
27
|
+
testEntityConfiguration,
|
|
28
|
+
} from '../../__tests__/fixtures/TestEntity';
|
|
29
|
+
import { EntityKnexDataManager } from '../EntityKnexDataManager';
|
|
30
|
+
|
|
31
|
+
describe(EntityKnexDataManager, () => {
|
|
32
|
+
it('loads by field equality conjunction and does not cache', async () => {
|
|
33
|
+
const queryContext = instance(mock<EntityQueryContext>());
|
|
34
|
+
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
|
|
35
|
+
PostgresEntityDatabaseAdapter,
|
|
36
|
+
);
|
|
37
|
+
when(
|
|
38
|
+
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
|
|
39
|
+
queryContext,
|
|
40
|
+
anything(),
|
|
41
|
+
anything(),
|
|
42
|
+
),
|
|
43
|
+
).thenResolve([
|
|
44
|
+
{
|
|
45
|
+
customIdField: '1',
|
|
46
|
+
testIndexedField: 'unique1',
|
|
47
|
+
stringField: 'hello',
|
|
48
|
+
intField: 1,
|
|
49
|
+
dateField: new Date(),
|
|
50
|
+
nullableField: null,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
customIdField: '2',
|
|
54
|
+
testIndexedField: 'unique2',
|
|
55
|
+
stringField: 'hello',
|
|
56
|
+
intField: 1,
|
|
57
|
+
dateField: new Date(),
|
|
58
|
+
nullableField: null,
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
62
|
+
testEntityConfiguration,
|
|
63
|
+
instance(databaseAdapterMock),
|
|
64
|
+
new NoOpEntityMetricsAdapter(),
|
|
65
|
+
TestEntity.name,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const entityDatas = await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
|
|
69
|
+
queryContext,
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
fieldName: 'stringField',
|
|
73
|
+
fieldValue: 'hello',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
fieldName: 'intField',
|
|
77
|
+
fieldValue: 1,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
{},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(entityDatas).toHaveLength(2);
|
|
84
|
+
|
|
85
|
+
verify(
|
|
86
|
+
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
|
|
87
|
+
queryContext,
|
|
88
|
+
anything(),
|
|
89
|
+
anything(),
|
|
90
|
+
),
|
|
91
|
+
).once();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('metrics', () => {
|
|
95
|
+
it('records metrics appropriately outside of transactions', async () => {
|
|
96
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
97
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
98
|
+
const queryContext = new StubQueryContextProvider().getQueryContext();
|
|
99
|
+
|
|
100
|
+
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
|
|
101
|
+
PostgresEntityDatabaseAdapter,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
when(
|
|
105
|
+
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
|
|
106
|
+
anything(),
|
|
107
|
+
anything(),
|
|
108
|
+
anything(),
|
|
109
|
+
),
|
|
110
|
+
).thenResolve([
|
|
111
|
+
{
|
|
112
|
+
customIdField: '1',
|
|
113
|
+
testIndexedField: 'unique1',
|
|
114
|
+
stringField: 'hello',
|
|
115
|
+
intField: 1,
|
|
116
|
+
dateField: new Date(),
|
|
117
|
+
nullableField: null,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
when(
|
|
121
|
+
databaseAdapterMock.fetchManyByRawWhereClauseAsync(
|
|
122
|
+
anything(),
|
|
123
|
+
anyString(),
|
|
124
|
+
anything(),
|
|
125
|
+
anything(),
|
|
126
|
+
),
|
|
127
|
+
).thenResolve([]);
|
|
128
|
+
|
|
129
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
130
|
+
testEntityConfiguration,
|
|
131
|
+
instance(databaseAdapterMock),
|
|
132
|
+
metricsAdapter,
|
|
133
|
+
TestEntity.name,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
|
|
137
|
+
queryContext,
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
fieldName: 'testIndexedField',
|
|
141
|
+
fieldValue: 'unique1',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
{},
|
|
145
|
+
);
|
|
146
|
+
verify(
|
|
147
|
+
metricsAdapterMock.logDataManagerLoadEvent(
|
|
148
|
+
deepEqual({
|
|
149
|
+
type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
|
|
150
|
+
isInTransaction: false,
|
|
151
|
+
entityClassName: TestEntity.name,
|
|
152
|
+
duration: anyNumber(),
|
|
153
|
+
count: 1,
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
).once();
|
|
157
|
+
|
|
158
|
+
resetCalls(metricsAdapterMock);
|
|
159
|
+
|
|
160
|
+
await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
|
|
161
|
+
verify(
|
|
162
|
+
metricsAdapterMock.logDataManagerLoadEvent(
|
|
163
|
+
deepEqual({
|
|
164
|
+
type: EntityMetricsLoadType.LOAD_MANY_RAW,
|
|
165
|
+
isInTransaction: false,
|
|
166
|
+
entityClassName: TestEntity.name,
|
|
167
|
+
duration: anyNumber(),
|
|
168
|
+
count: 0,
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
).once();
|
|
172
|
+
|
|
173
|
+
verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('records metrics appropriately inside of transactions', async () => {
|
|
177
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
178
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
179
|
+
|
|
180
|
+
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
|
|
181
|
+
PostgresEntityDatabaseAdapter,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
when(
|
|
185
|
+
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
|
|
186
|
+
anything(),
|
|
187
|
+
anything(),
|
|
188
|
+
anything(),
|
|
189
|
+
),
|
|
190
|
+
).thenResolve([
|
|
191
|
+
{
|
|
192
|
+
customIdField: '1',
|
|
193
|
+
testIndexedField: 'unique1',
|
|
194
|
+
stringField: 'hello',
|
|
195
|
+
intField: 1,
|
|
196
|
+
dateField: new Date(),
|
|
197
|
+
nullableField: null,
|
|
198
|
+
},
|
|
199
|
+
]);
|
|
200
|
+
when(
|
|
201
|
+
databaseAdapterMock.fetchManyByRawWhereClauseAsync(
|
|
202
|
+
anything(),
|
|
203
|
+
anyString(),
|
|
204
|
+
anything(),
|
|
205
|
+
anything(),
|
|
206
|
+
),
|
|
207
|
+
).thenResolve([]);
|
|
208
|
+
|
|
209
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
210
|
+
testEntityConfiguration,
|
|
211
|
+
instance(databaseAdapterMock),
|
|
212
|
+
metricsAdapter,
|
|
213
|
+
TestEntity.name,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
|
|
217
|
+
await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
|
|
218
|
+
queryContext,
|
|
219
|
+
[
|
|
220
|
+
{
|
|
221
|
+
fieldName: 'testIndexedField',
|
|
222
|
+
fieldValue: 'unique1',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
{},
|
|
226
|
+
);
|
|
227
|
+
verify(
|
|
228
|
+
metricsAdapterMock.logDataManagerLoadEvent(
|
|
229
|
+
deepEqual({
|
|
230
|
+
type: EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
|
|
231
|
+
isInTransaction: true,
|
|
232
|
+
entityClassName: TestEntity.name,
|
|
233
|
+
duration: anyNumber(),
|
|
234
|
+
count: 1,
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
).once();
|
|
238
|
+
|
|
239
|
+
resetCalls(metricsAdapterMock);
|
|
240
|
+
|
|
241
|
+
await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
|
|
242
|
+
verify(
|
|
243
|
+
metricsAdapterMock.logDataManagerLoadEvent(
|
|
244
|
+
deepEqual({
|
|
245
|
+
type: EntityMetricsLoadType.LOAD_MANY_RAW,
|
|
246
|
+
isInTransaction: true,
|
|
247
|
+
entityClassName: TestEntity.name,
|
|
248
|
+
duration: anyNumber(),
|
|
249
|
+
count: 0,
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
252
|
+
).once();
|
|
253
|
+
|
|
254
|
+
verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('pagination', () => {
|
|
260
|
+
describe('max page size validation', () => {
|
|
261
|
+
it('should throw when first exceeds maxPageSize', async () => {
|
|
262
|
+
const queryContext = instance(mock<EntityQueryContext>());
|
|
263
|
+
const databaseAdapterMock = mock<
|
|
264
|
+
PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>
|
|
265
|
+
>(PostgresEntityDatabaseAdapter);
|
|
266
|
+
|
|
267
|
+
// Configure the adapter to return a maxPageSize of 100
|
|
268
|
+
when(databaseAdapterMock.paginationMaxPageSize).thenReturn(100);
|
|
269
|
+
|
|
270
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
271
|
+
testEntityConfiguration,
|
|
272
|
+
instance(databaseAdapterMock),
|
|
273
|
+
new NoOpEntityMetricsAdapter(),
|
|
274
|
+
TestEntity.name,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await expect(
|
|
278
|
+
entityDataManager.loadPageAsync(queryContext, {
|
|
279
|
+
first: 101,
|
|
280
|
+
pagination: {
|
|
281
|
+
strategy: PaginationStrategy.STANDARD,
|
|
282
|
+
orderBy: [{ fieldName: 'customIdField', order: OrderByOrdering.ASCENDING }],
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
).rejects.toThrow('first must not exceed maximum page size of 100');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should throw when last exceeds maxPageSize', async () => {
|
|
289
|
+
const queryContext = instance(mock<EntityQueryContext>());
|
|
290
|
+
const databaseAdapterMock = mock<
|
|
291
|
+
PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>
|
|
292
|
+
>(PostgresEntityDatabaseAdapter);
|
|
293
|
+
|
|
294
|
+
// Configure the adapter to return a maxPageSize of 100
|
|
295
|
+
when(databaseAdapterMock.paginationMaxPageSize).thenReturn(100);
|
|
296
|
+
|
|
297
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
298
|
+
testEntityConfiguration,
|
|
299
|
+
instance(databaseAdapterMock),
|
|
300
|
+
new NoOpEntityMetricsAdapter(),
|
|
301
|
+
TestEntity.name,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await expect(
|
|
305
|
+
entityDataManager.loadPageAsync(queryContext, {
|
|
306
|
+
last: 101,
|
|
307
|
+
pagination: {
|
|
308
|
+
strategy: PaginationStrategy.STANDARD,
|
|
309
|
+
orderBy: [{ fieldName: 'customIdField', order: OrderByOrdering.ASCENDING }],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
).rejects.toThrow('last must not exceed maximum page size of 100');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should allow first/last within maxPageSize', async () => {
|
|
316
|
+
const queryContext = instance(mock<EntityQueryContext>());
|
|
317
|
+
const databaseAdapterMock = mock<
|
|
318
|
+
PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>
|
|
319
|
+
>(PostgresEntityDatabaseAdapter);
|
|
320
|
+
|
|
321
|
+
// Configure the adapter to return a maxPageSize of 100
|
|
322
|
+
when(databaseAdapterMock.paginationMaxPageSize).thenReturn(100);
|
|
323
|
+
when(
|
|
324
|
+
databaseAdapterMock.fetchManyBySQLFragmentAsync(queryContext, anything(), anything()),
|
|
325
|
+
).thenResolve([]);
|
|
326
|
+
|
|
327
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
328
|
+
testEntityConfiguration,
|
|
329
|
+
instance(databaseAdapterMock),
|
|
330
|
+
new NoOpEntityMetricsAdapter(),
|
|
331
|
+
TestEntity.name,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// This should not throw
|
|
335
|
+
const result = await entityDataManager.loadPageAsync(queryContext, {
|
|
336
|
+
first: 100,
|
|
337
|
+
pagination: {
|
|
338
|
+
strategy: PaginationStrategy.STANDARD,
|
|
339
|
+
orderBy: [{ fieldName: 'customIdField', order: OrderByOrdering.ASCENDING }],
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(result.edges).toEqual([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should allow pagination when maxPageSize is not configured', async () => {
|
|
347
|
+
const queryContext = instance(mock<EntityQueryContext>());
|
|
348
|
+
const databaseAdapterMock = mock<
|
|
349
|
+
PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>
|
|
350
|
+
>(PostgresEntityDatabaseAdapter);
|
|
351
|
+
|
|
352
|
+
// Configure the adapter to return undefined for maxPageSize
|
|
353
|
+
when(databaseAdapterMock.paginationMaxPageSize).thenReturn(undefined);
|
|
354
|
+
when(
|
|
355
|
+
databaseAdapterMock.fetchManyBySQLFragmentAsync(queryContext, anything(), anything()),
|
|
356
|
+
).thenResolve([]);
|
|
357
|
+
|
|
358
|
+
const entityDataManager = new EntityKnexDataManager(
|
|
359
|
+
testEntityConfiguration,
|
|
360
|
+
instance(databaseAdapterMock),
|
|
361
|
+
new NoOpEntityMetricsAdapter(),
|
|
362
|
+
TestEntity.name,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// This should not throw even with a large page size
|
|
366
|
+
const result = await entityDataManager.loadPageAsync(queryContext, {
|
|
367
|
+
first: 10000,
|
|
368
|
+
pagination: {
|
|
369
|
+
strategy: PaginationStrategy.STANDARD,
|
|
370
|
+
orderBy: [{ fieldName: 'customIdField', order: OrderByOrdering.ASCENDING }],
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
expect(result.edges).toEqual([]);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
import { computeIfAbsentInWeakMap } from '../weakMaps';
|
|
4
|
+
|
|
5
|
+
describe(computeIfAbsentInWeakMap, () => {
|
|
6
|
+
it('computes a value when absent', () => {
|
|
7
|
+
const map = new WeakMap<object, string>();
|
|
8
|
+
const key = {};
|
|
9
|
+
const blah = computeIfAbsentInWeakMap(map, key, () => 'world');
|
|
10
|
+
expect(blah).toEqual(map.get(key));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not compute a value when already present', () => {
|
|
14
|
+
const map = new WeakMap<object, string>();
|
|
15
|
+
const key = {};
|
|
16
|
+
map.set(key, 'world');
|
|
17
|
+
let didCompute = false;
|
|
18
|
+
const blah = computeIfAbsentInWeakMap(map, key, () => {
|
|
19
|
+
didCompute = true;
|
|
20
|
+
return 'world2';
|
|
21
|
+
});
|
|
22
|
+
expect(blah).toEqual(map.get(key));
|
|
23
|
+
expect(didCompute).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EntityDatabaseAdapter, EntityTableDataCoordinator } from '@expo/entity';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
|
|
4
|
+
import { BasePostgresEntityDatabaseAdapter } from '../BasePostgresEntityDatabaseAdapter';
|
|
5
|
+
import { EntityKnexDataManager } from './EntityKnexDataManager';
|
|
6
|
+
import { computeIfAbsentInWeakMap } from './weakMaps';
|
|
7
|
+
|
|
8
|
+
const knexDataManagerCache = new WeakMap<
|
|
9
|
+
EntityTableDataCoordinator<any, any>,
|
|
10
|
+
EntityKnexDataManager<any, any>
|
|
11
|
+
>();
|
|
12
|
+
|
|
13
|
+
export function getKnexDataManager<
|
|
14
|
+
TFields extends Record<string, any>,
|
|
15
|
+
TIDField extends keyof TFields,
|
|
16
|
+
>(
|
|
17
|
+
tableDataCoordinator: EntityTableDataCoordinator<TFields, TIDField>,
|
|
18
|
+
): EntityKnexDataManager<TFields, TIDField> {
|
|
19
|
+
return computeIfAbsentInWeakMap(
|
|
20
|
+
knexDataManagerCache,
|
|
21
|
+
tableDataCoordinator,
|
|
22
|
+
(coordinator) =>
|
|
23
|
+
new EntityKnexDataManager(
|
|
24
|
+
coordinator.entityConfiguration,
|
|
25
|
+
requireBasePostgresAdapter(coordinator.databaseAdapter),
|
|
26
|
+
coordinator.metricsAdapter,
|
|
27
|
+
coordinator.entityClassName,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requireBasePostgresAdapter<
|
|
33
|
+
TFields extends Record<string, any>,
|
|
34
|
+
TIDField extends keyof TFields,
|
|
35
|
+
>(
|
|
36
|
+
databaseAdapter: EntityDatabaseAdapter<TFields, TIDField>,
|
|
37
|
+
): BasePostgresEntityDatabaseAdapter<TFields, TIDField> {
|
|
38
|
+
assert(
|
|
39
|
+
databaseAdapter instanceof BasePostgresEntityDatabaseAdapter,
|
|
40
|
+
`Cannot create KnexDataManager for EntityTableDataCoordinator with non-Postgres database adapter.`,
|
|
41
|
+
);
|
|
42
|
+
return databaseAdapter;
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityCompanion,
|
|
3
|
+
EntityPrivacyPolicy,
|
|
4
|
+
IEntityClass,
|
|
5
|
+
ReadonlyEntity,
|
|
6
|
+
ViewerContext,
|
|
7
|
+
} from '@expo/entity';
|
|
8
|
+
|
|
9
|
+
import { KnexEntityLoaderFactory } from '../KnexEntityLoaderFactory';
|
|
10
|
+
import { getKnexDataManager } from './getKnexDataManager';
|
|
11
|
+
import { computeIfAbsentInWeakMap } from './weakMaps';
|
|
12
|
+
|
|
13
|
+
const knexEntityLoaderFactoryCache = new WeakMap<
|
|
14
|
+
EntityCompanion<any, any, any, any, any, any>,
|
|
15
|
+
KnexEntityLoaderFactory<any, any, any, any, any, any>
|
|
16
|
+
>();
|
|
17
|
+
|
|
18
|
+
export function getKnexEntityLoaderFactory<
|
|
19
|
+
TFields extends Record<string, any>,
|
|
20
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
21
|
+
TViewerContext extends ViewerContext,
|
|
22
|
+
TViewerContext2 extends TViewerContext,
|
|
23
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
24
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
25
|
+
TFields,
|
|
26
|
+
TIDField,
|
|
27
|
+
TViewerContext,
|
|
28
|
+
TEntity,
|
|
29
|
+
TSelectedFields
|
|
30
|
+
>,
|
|
31
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
32
|
+
>(
|
|
33
|
+
entityClass: IEntityClass<
|
|
34
|
+
TFields,
|
|
35
|
+
TIDField,
|
|
36
|
+
TViewerContext,
|
|
37
|
+
TEntity,
|
|
38
|
+
TPrivacyPolicy,
|
|
39
|
+
TSelectedFields
|
|
40
|
+
>,
|
|
41
|
+
viewerContext: TViewerContext2,
|
|
42
|
+
): KnexEntityLoaderFactory<
|
|
43
|
+
TFields,
|
|
44
|
+
TIDField,
|
|
45
|
+
TViewerContext,
|
|
46
|
+
TEntity,
|
|
47
|
+
TPrivacyPolicy,
|
|
48
|
+
TSelectedFields
|
|
49
|
+
> {
|
|
50
|
+
return computeIfAbsentInWeakMap(
|
|
51
|
+
knexEntityLoaderFactoryCache,
|
|
52
|
+
viewerContext.entityCompanionProvider.getCompanionForEntity(entityClass),
|
|
53
|
+
(companion) =>
|
|
54
|
+
new KnexEntityLoaderFactory(
|
|
55
|
+
companion,
|
|
56
|
+
getKnexDataManager(companion.tableDataCoordinator),
|
|
57
|
+
companion.metricsAdapter,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* c8 ignore start - types only */
|
|
2
|
+
|
|
3
|
+
export type NonNullableKeys<T> = {
|
|
4
|
+
[K in keyof T]: T[K] extends NonNullable<T[K]> ? K : never;
|
|
5
|
+
}[keyof T];
|
|
6
|
+
|
|
7
|
+
export type PickNonNullable<T> = Pick<T, NonNullableKeys<T>>;
|
|
8
|
+
|
|
9
|
+
export type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
|
10
|
+
|
|
11
|
+
/* c8 ignore stop - types only */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If the specified key is not already associated with a value in this weak map, computes
|
|
3
|
+
* its value using the given mapping function and enters it into this map.
|
|
4
|
+
*
|
|
5
|
+
* @param map - map from which to get the key's value or compute and associate
|
|
6
|
+
* @param key - key for which to get the value or with which the computed value is to be associated
|
|
7
|
+
* @param mappingFunction - function to compute a value for key
|
|
8
|
+
*/
|
|
9
|
+
export const computeIfAbsentInWeakMap = <K extends WeakKey, V>(
|
|
10
|
+
map: WeakMap<K, V>,
|
|
11
|
+
key: K,
|
|
12
|
+
mappingFunction: (key: K) => V,
|
|
13
|
+
): V => {
|
|
14
|
+
if (!map.has(key)) {
|
|
15
|
+
const value = mappingFunction(key);
|
|
16
|
+
map.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
return map.get(key)!;
|
|
19
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityPrivacyPolicy,
|
|
3
|
+
EntityQueryContext,
|
|
4
|
+
IEntityClass,
|
|
5
|
+
ReadonlyEntity,
|
|
6
|
+
ViewerContext,
|
|
7
|
+
} from '@expo/entity';
|
|
8
|
+
|
|
9
|
+
import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader';
|
|
10
|
+
import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader';
|
|
11
|
+
import { getKnexEntityLoaderFactory } from './internal/getKnexEntityLoaderFactory';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Vend knex loader for loading entities via non-data-loader methods in a given query context.
|
|
15
|
+
* @param entityClass - entity class to load
|
|
16
|
+
* @param viewerContext - viewer context of loading user
|
|
17
|
+
* @param queryContext - query context in which to perform the load
|
|
18
|
+
*/
|
|
19
|
+
export function knexLoader<
|
|
20
|
+
TMFields extends object,
|
|
21
|
+
TMIDField extends keyof NonNullable<Pick<TMFields, TMSelectedFields>>,
|
|
22
|
+
TMViewerContext extends ViewerContext,
|
|
23
|
+
TMViewerContext2 extends TMViewerContext,
|
|
24
|
+
TMEntity extends ReadonlyEntity<TMFields, TMIDField, TMViewerContext, TMSelectedFields>,
|
|
25
|
+
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
|
26
|
+
TMFields,
|
|
27
|
+
TMIDField,
|
|
28
|
+
TMViewerContext,
|
|
29
|
+
TMEntity,
|
|
30
|
+
TMSelectedFields
|
|
31
|
+
>,
|
|
32
|
+
TMSelectedFields extends keyof TMFields = keyof TMFields,
|
|
33
|
+
>(
|
|
34
|
+
entityClass: IEntityClass<
|
|
35
|
+
TMFields,
|
|
36
|
+
TMIDField,
|
|
37
|
+
TMViewerContext,
|
|
38
|
+
TMEntity,
|
|
39
|
+
TMPrivacyPolicy,
|
|
40
|
+
TMSelectedFields
|
|
41
|
+
>,
|
|
42
|
+
viewerContext: TMViewerContext2,
|
|
43
|
+
queryContext: EntityQueryContext = viewerContext
|
|
44
|
+
.getViewerScopedEntityCompanionForClass(entityClass)
|
|
45
|
+
.getQueryContextProvider()
|
|
46
|
+
.getQueryContext(),
|
|
47
|
+
): EnforcingKnexEntityLoader<
|
|
48
|
+
TMFields,
|
|
49
|
+
TMIDField,
|
|
50
|
+
TMViewerContext,
|
|
51
|
+
TMEntity,
|
|
52
|
+
TMPrivacyPolicy,
|
|
53
|
+
TMSelectedFields
|
|
54
|
+
> {
|
|
55
|
+
return getKnexEntityLoaderFactory(entityClass, viewerContext).forLoadEnforcing(
|
|
56
|
+
viewerContext,
|
|
57
|
+
queryContext,
|
|
58
|
+
{ previousValue: null, cascadingDeleteCause: null },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Vend knex loader for loading entities via non-data-loader methods in a given query context.
|
|
64
|
+
* Returns authorization results instead of throwing on authorization errors.
|
|
65
|
+
* @param entityClass - entity class to load
|
|
66
|
+
* @param viewerContext - viewer context of loading user
|
|
67
|
+
* @param queryContext - query context in which to perform the load
|
|
68
|
+
*/
|
|
69
|
+
export function knexLoaderWithAuthorizationResults<
|
|
70
|
+
TMFields extends object,
|
|
71
|
+
TMIDField extends keyof NonNullable<Pick<TMFields, TMSelectedFields>>,
|
|
72
|
+
TMViewerContext extends ViewerContext,
|
|
73
|
+
TMViewerContext2 extends TMViewerContext,
|
|
74
|
+
TMEntity extends ReadonlyEntity<TMFields, TMIDField, TMViewerContext, TMSelectedFields>,
|
|
75
|
+
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
|
76
|
+
TMFields,
|
|
77
|
+
TMIDField,
|
|
78
|
+
TMViewerContext,
|
|
79
|
+
TMEntity,
|
|
80
|
+
TMSelectedFields
|
|
81
|
+
>,
|
|
82
|
+
TMSelectedFields extends keyof TMFields = keyof TMFields,
|
|
83
|
+
>(
|
|
84
|
+
entityClass: IEntityClass<
|
|
85
|
+
TMFields,
|
|
86
|
+
TMIDField,
|
|
87
|
+
TMViewerContext,
|
|
88
|
+
TMEntity,
|
|
89
|
+
TMPrivacyPolicy,
|
|
90
|
+
TMSelectedFields
|
|
91
|
+
>,
|
|
92
|
+
viewerContext: TMViewerContext2,
|
|
93
|
+
queryContext: EntityQueryContext = viewerContext
|
|
94
|
+
.getViewerScopedEntityCompanionForClass(entityClass)
|
|
95
|
+
.getQueryContextProvider()
|
|
96
|
+
.getQueryContext(),
|
|
97
|
+
): AuthorizationResultBasedKnexEntityLoader<
|
|
98
|
+
TMFields,
|
|
99
|
+
TMIDField,
|
|
100
|
+
TMViewerContext,
|
|
101
|
+
TMEntity,
|
|
102
|
+
TMPrivacyPolicy,
|
|
103
|
+
TMSelectedFields
|
|
104
|
+
> {
|
|
105
|
+
return getKnexEntityLoaderFactory(entityClass, viewerContext).forLoad(
|
|
106
|
+
viewerContext,
|
|
107
|
+
queryContext,
|
|
108
|
+
{ previousValue: null, cascadingDeleteCause: null },
|
|
109
|
+
);
|
|
110
|
+
}
|