@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,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityDatabaseAdapter,
|
|
3
|
+
EntityQueryContext,
|
|
4
|
+
getDatabaseFieldForEntityField,
|
|
5
|
+
transformDatabaseObjectToFields,
|
|
6
|
+
} from '@expo/entity';
|
|
7
|
+
import { Knex } from 'knex';
|
|
8
|
+
|
|
9
|
+
import { SQLFragment } from './SQLOperator';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Equality operand that is used for selecting entities with a field with a single value.
|
|
13
|
+
*/
|
|
14
|
+
export interface SingleValueFieldEqualityCondition<
|
|
15
|
+
TFields extends Record<string, any>,
|
|
16
|
+
N extends keyof TFields = keyof TFields,
|
|
17
|
+
> {
|
|
18
|
+
fieldName: N;
|
|
19
|
+
fieldValue: TFields[N];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Equality operand that is used for selecting entities with a field matching one of multiple values.
|
|
24
|
+
*/
|
|
25
|
+
export interface MultiValueFieldEqualityCondition<
|
|
26
|
+
TFields extends Record<string, any>,
|
|
27
|
+
N extends keyof TFields = keyof TFields,
|
|
28
|
+
> {
|
|
29
|
+
fieldName: N;
|
|
30
|
+
fieldValues: readonly TFields[N][];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A single equality operand for use in a selection clause.
|
|
35
|
+
* See EntityLoader.loadManyByFieldEqualityConjunctionAsync documentation for examples.
|
|
36
|
+
*/
|
|
37
|
+
export type FieldEqualityCondition<
|
|
38
|
+
TFields extends Record<string, any>,
|
|
39
|
+
N extends keyof TFields = keyof TFields,
|
|
40
|
+
> = SingleValueFieldEqualityCondition<TFields, N> | MultiValueFieldEqualityCondition<TFields, N>;
|
|
41
|
+
|
|
42
|
+
export function isSingleValueFieldEqualityCondition<
|
|
43
|
+
TFields extends Record<string, any>,
|
|
44
|
+
N extends keyof TFields = keyof TFields,
|
|
45
|
+
>(
|
|
46
|
+
condition: FieldEqualityCondition<TFields, N>,
|
|
47
|
+
): condition is SingleValueFieldEqualityCondition<TFields, N> {
|
|
48
|
+
return (condition as SingleValueFieldEqualityCondition<TFields, N>).fieldValue !== undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TableFieldSingleValueEqualityCondition {
|
|
52
|
+
tableField: string;
|
|
53
|
+
tableValue: any;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TableFieldMultiValueEqualityCondition {
|
|
57
|
+
tableField: string;
|
|
58
|
+
tableValues: readonly any[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export enum NullsOrdering {
|
|
62
|
+
FIRST = 'first',
|
|
63
|
+
LAST = 'last',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ordering options for `orderBy` clauses.
|
|
68
|
+
*/
|
|
69
|
+
export enum OrderByOrdering {
|
|
70
|
+
/**
|
|
71
|
+
* Ascending order (lowest to highest).
|
|
72
|
+
* Ascending order puts smaller values first, where "smaller" is defined in terms of the %3C operator.
|
|
73
|
+
*/
|
|
74
|
+
ASCENDING = 'asc',
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Descending order (highest to lowest).
|
|
78
|
+
* Descending order puts larger values first, where "larger" is defined in terms of the %3E operator.
|
|
79
|
+
*/
|
|
80
|
+
DESCENDING = 'desc',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type PostgresOrderByClause<TFields extends Record<string, any>> =
|
|
84
|
+
| {
|
|
85
|
+
/**
|
|
86
|
+
* The field name to order by.
|
|
87
|
+
*/
|
|
88
|
+
fieldName: keyof TFields;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The OrderByOrdering to order by.
|
|
92
|
+
*/
|
|
93
|
+
order: OrderByOrdering;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optional nulls ordering. If not provided, the database default is used
|
|
97
|
+
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
|
|
98
|
+
*/
|
|
99
|
+
nulls?: NullsOrdering | undefined;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
/**
|
|
103
|
+
* A raw SQL fragment to order by. May not contain ASC or DESC, as ordering direction is determined by the `order` property.
|
|
104
|
+
*/
|
|
105
|
+
fieldFragment: SQLFragment<TFields>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The OrderByOrdering to order by.
|
|
109
|
+
*/
|
|
110
|
+
order: OrderByOrdering;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Optional nulls ordering. If not provided, the database default is used
|
|
114
|
+
* (NULLS LAST for ASC, NULLS FIRST for DESC in PostgreSQL).
|
|
115
|
+
*/
|
|
116
|
+
nulls?: NullsOrdering | undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* SQL modifiers that only affect the selection but not the projection.
|
|
121
|
+
*/
|
|
122
|
+
export interface PostgresQuerySelectionModifiers<TFields extends Record<string, any>> {
|
|
123
|
+
/**
|
|
124
|
+
* Order the entities by specified columns and orders.
|
|
125
|
+
*/
|
|
126
|
+
orderBy?: readonly PostgresOrderByClause<TFields>[];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Skip the specified number of entities queried before returning.
|
|
130
|
+
*/
|
|
131
|
+
offset?: number;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Limit the number of entities returned.
|
|
135
|
+
*/
|
|
136
|
+
limit?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type TableOrderByClause<TFields extends Record<string, any>> =
|
|
140
|
+
| {
|
|
141
|
+
columnName: string;
|
|
142
|
+
order: OrderByOrdering;
|
|
143
|
+
nulls: NullsOrdering | undefined;
|
|
144
|
+
}
|
|
145
|
+
| {
|
|
146
|
+
columnFragment: SQLFragment<TFields>;
|
|
147
|
+
order: OrderByOrdering;
|
|
148
|
+
nulls: NullsOrdering | undefined;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export interface TableQuerySelectionModifiers<TFields extends Record<string, any>> {
|
|
152
|
+
orderBy: TableOrderByClause<TFields>[] | undefined;
|
|
153
|
+
offset: number | undefined;
|
|
154
|
+
limit: number | undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export abstract class BasePostgresEntityDatabaseAdapter<
|
|
158
|
+
TFields extends Record<string, any>,
|
|
159
|
+
TIDField extends keyof TFields,
|
|
160
|
+
> extends EntityDatabaseAdapter<TFields, TIDField> {
|
|
161
|
+
/**
|
|
162
|
+
* Get the maximum page size for pagination.
|
|
163
|
+
* @returns maximum page size if configured, undefined otherwise
|
|
164
|
+
*/
|
|
165
|
+
get paginationMaxPageSize(): number | undefined {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Fetch many objects matching the conjunction of where clauses constructed from
|
|
170
|
+
* specified field equality operands.
|
|
171
|
+
*
|
|
172
|
+
* @param queryContext - query context with which to perform the fetch
|
|
173
|
+
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
|
174
|
+
* @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
|
|
175
|
+
* @returns array of objects matching the query
|
|
176
|
+
*/
|
|
177
|
+
async fetchManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(
|
|
178
|
+
queryContext: EntityQueryContext,
|
|
179
|
+
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
|
180
|
+
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
181
|
+
): Promise<readonly Readonly<TFields>[]> {
|
|
182
|
+
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
|
|
183
|
+
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
|
|
184
|
+
for (const operand of fieldEqualityOperands) {
|
|
185
|
+
if (isSingleValueFieldEqualityCondition(operand)) {
|
|
186
|
+
tableFieldSingleValueOperands.push({
|
|
187
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
188
|
+
tableValue: operand.fieldValue,
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
tableFieldMultipleValueOperands.push({
|
|
192
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
193
|
+
tableValues: operand.fieldValues,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
|
|
199
|
+
queryContext.getQueryInterface(),
|
|
200
|
+
this.entityConfiguration.tableName,
|
|
201
|
+
tableFieldSingleValueOperands,
|
|
202
|
+
tableFieldMultipleValueOperands,
|
|
203
|
+
this.convertToTableQueryModifiers(querySelectionModifiers),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return results.map((result) =>
|
|
207
|
+
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
protected abstract fetchManyByFieldEqualityConjunctionInternalAsync(
|
|
212
|
+
queryInterface: Knex,
|
|
213
|
+
tableName: string,
|
|
214
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
215
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
216
|
+
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
217
|
+
): Promise<object[]>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Fetch many objects matching the raw WHERE clause.
|
|
221
|
+
*
|
|
222
|
+
* @param queryContext - query context with which to perform the fetch
|
|
223
|
+
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
|
|
224
|
+
* @param bindings - array of positional bindings or object of named bindings
|
|
225
|
+
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
|
226
|
+
* @returns array of objects matching the query
|
|
227
|
+
*/
|
|
228
|
+
async fetchManyByRawWhereClauseAsync(
|
|
229
|
+
queryContext: EntityQueryContext,
|
|
230
|
+
rawWhereClause: string,
|
|
231
|
+
bindings: any[] | object,
|
|
232
|
+
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
233
|
+
): Promise<readonly Readonly<TFields>[]> {
|
|
234
|
+
const results = await this.fetchManyByRawWhereClauseInternalAsync(
|
|
235
|
+
queryContext.getQueryInterface(),
|
|
236
|
+
this.entityConfiguration.tableName,
|
|
237
|
+
rawWhereClause,
|
|
238
|
+
bindings,
|
|
239
|
+
this.convertToTableQueryModifiers(querySelectionModifiers),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return results.map((result) =>
|
|
243
|
+
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
protected abstract fetchManyByRawWhereClauseInternalAsync(
|
|
248
|
+
queryInterface: Knex,
|
|
249
|
+
tableName: string,
|
|
250
|
+
rawWhereClause: string,
|
|
251
|
+
bindings: object | any[],
|
|
252
|
+
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
253
|
+
): Promise<object[]>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Fetch many objects matching the SQL fragment.
|
|
257
|
+
*
|
|
258
|
+
* @param queryContext - query context with which to perform the fetch
|
|
259
|
+
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
260
|
+
* @param querySelectionModifiers - limit, offset, and orderByFragment for the query
|
|
261
|
+
* @returns array of objects matching the query
|
|
262
|
+
*/
|
|
263
|
+
async fetchManyBySQLFragmentAsync(
|
|
264
|
+
queryContext: EntityQueryContext,
|
|
265
|
+
sqlFragment: SQLFragment<TFields>,
|
|
266
|
+
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
267
|
+
): Promise<readonly Readonly<TFields>[]> {
|
|
268
|
+
const results = await this.fetchManyBySQLFragmentInternalAsync(
|
|
269
|
+
queryContext.getQueryInterface(),
|
|
270
|
+
this.entityConfiguration.tableName,
|
|
271
|
+
sqlFragment,
|
|
272
|
+
this.convertToTableQueryModifiers(querySelectionModifiers),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return results.map((result) =>
|
|
276
|
+
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
protected abstract fetchManyBySQLFragmentInternalAsync(
|
|
281
|
+
queryInterface: Knex,
|
|
282
|
+
tableName: string,
|
|
283
|
+
sqlFragment: SQLFragment<TFields>,
|
|
284
|
+
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
285
|
+
): Promise<object[]>;
|
|
286
|
+
|
|
287
|
+
private convertToTableQueryModifiers(
|
|
288
|
+
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
289
|
+
): TableQuerySelectionModifiers<TFields> {
|
|
290
|
+
const orderBy = querySelectionModifiers.orderBy;
|
|
291
|
+
return {
|
|
292
|
+
orderBy:
|
|
293
|
+
orderBy !== undefined
|
|
294
|
+
? orderBy.map((orderBySpecification): TableOrderByClause<TFields> => {
|
|
295
|
+
if ('fieldName' in orderBySpecification) {
|
|
296
|
+
return {
|
|
297
|
+
columnName: getDatabaseFieldForEntityField(
|
|
298
|
+
this.entityConfiguration,
|
|
299
|
+
orderBySpecification.fieldName,
|
|
300
|
+
),
|
|
301
|
+
order: orderBySpecification.order,
|
|
302
|
+
nulls: orderBySpecification.nulls,
|
|
303
|
+
};
|
|
304
|
+
} else {
|
|
305
|
+
return {
|
|
306
|
+
columnFragment: orderBySpecification.fieldFragment,
|
|
307
|
+
order: orderBySpecification.order,
|
|
308
|
+
nulls: orderBySpecification.nulls,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
: undefined,
|
|
313
|
+
offset: querySelectionModifiers.offset,
|
|
314
|
+
limit: querySelectionModifiers.limit,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityLoaderOrderByClause,
|
|
3
|
+
EntityLoaderQuerySelectionModifiers,
|
|
4
|
+
} from './AuthorizationResultBasedKnexEntityLoader';
|
|
5
|
+
import { NullsOrdering, OrderByOrdering } from './BasePostgresEntityDatabaseAdapter';
|
|
6
|
+
import { SQLFragment } from './SQLOperator';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base SQL query builder that provides common functionality for building SQL queries.
|
|
10
|
+
*/
|
|
11
|
+
export abstract class BaseSQLQueryBuilder<
|
|
12
|
+
TFields extends Record<string, any>,
|
|
13
|
+
TSelectedFields extends keyof TFields,
|
|
14
|
+
TResultType,
|
|
15
|
+
> {
|
|
16
|
+
private executed = false;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
|
20
|
+
private readonly modifiers: {
|
|
21
|
+
limit?: number;
|
|
22
|
+
offset?: number;
|
|
23
|
+
orderBy?: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
|
|
24
|
+
},
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Limit the number of results
|
|
29
|
+
*/
|
|
30
|
+
limit(n: number): this {
|
|
31
|
+
this.modifiers.limit = n;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Skip a number of results
|
|
37
|
+
*/
|
|
38
|
+
offset(n: number): this {
|
|
39
|
+
this.modifiers.offset = n;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Order by a field. Can be called multiple times to add multiple order bys.
|
|
45
|
+
*/
|
|
46
|
+
orderBy(
|
|
47
|
+
fieldName: TSelectedFields,
|
|
48
|
+
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
|
49
|
+
nulls: NullsOrdering | undefined = undefined,
|
|
50
|
+
): this {
|
|
51
|
+
this.modifiers.orderBy = [...(this.modifiers.orderBy ?? []), { fieldName, order, nulls }];
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Order by a SQL fragment expression.
|
|
57
|
+
* Provides type-safe, parameterized ORDER BY clauses
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* query.orderByFragment(
|
|
62
|
+
* sql`(data->>'createdAt')::timestamp`,
|
|
63
|
+
* OrderByOrdering.DESCENDING,
|
|
64
|
+
* );
|
|
65
|
+
* // Generates ORDER BY clause that orders by the createdAt field in the JSONB data column, cast to a timestamp, in descending order.
|
|
66
|
+
* // Note that the SQL fragment is parameterized, so it is safe from SQL injection.
|
|
67
|
+
* // The generated SQL would look like: ORDER BY (data->>'createdAt')::timestamp DESC
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @param fragment - The SQL fragment to order by. Must not include the ASC/DESC keyword, as ordering direction is determined by the `order` parameter.
|
|
71
|
+
* @param order - The ordering direction (ascending or descending). Defaults to ascending.
|
|
72
|
+
*/
|
|
73
|
+
orderBySQL(
|
|
74
|
+
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
|
75
|
+
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
|
76
|
+
nulls: NullsOrdering | undefined = undefined,
|
|
77
|
+
): this {
|
|
78
|
+
this.modifiers.orderBy = [
|
|
79
|
+
...(this.modifiers.orderBy ?? []),
|
|
80
|
+
{ fieldFragment: fragment, order, nulls },
|
|
81
|
+
];
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the current modifiers as QuerySelectionModifiersWithOrderByFragment<TFields>
|
|
87
|
+
*/
|
|
88
|
+
protected getModifiers(): EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> {
|
|
89
|
+
return this.modifiers;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the SQL fragment
|
|
94
|
+
*/
|
|
95
|
+
protected getSQLFragment(): SQLFragment<Pick<TFields, TSelectedFields>> {
|
|
96
|
+
return this.sqlFragment;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Execute the query and return results.
|
|
101
|
+
* Implementation depends on the specific loader type.
|
|
102
|
+
*/
|
|
103
|
+
public async executeAsync(): Promise<readonly TResultType[]> {
|
|
104
|
+
if (this.executed) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'Query has already been executed. Create a new query builder to execute again.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
this.executed = true;
|
|
110
|
+
return await this.executeInternalAsync();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected abstract executeInternalAsync(): Promise<readonly TResultType[]>;
|
|
114
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityConstructionUtils,
|
|
3
|
+
EntityPrivacyPolicy,
|
|
4
|
+
EntityQueryContext,
|
|
5
|
+
IEntityMetricsAdapter,
|
|
6
|
+
ReadonlyEntity,
|
|
7
|
+
ViewerContext,
|
|
8
|
+
} from '@expo/entity';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
AuthorizationResultBasedKnexEntityLoader,
|
|
12
|
+
EntityLoaderLoadPageArgs,
|
|
13
|
+
EntityLoaderQuerySelectionModifiers,
|
|
14
|
+
} from './AuthorizationResultBasedKnexEntityLoader';
|
|
15
|
+
import { FieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter';
|
|
16
|
+
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder';
|
|
17
|
+
import { SQLFragment } from './SQLOperator';
|
|
18
|
+
import type { Connection, EntityKnexDataManager } from './internal/EntityKnexDataManager';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enforcing knex entity loader for non-data-loader-based load methods.
|
|
22
|
+
* All loads through this loader will throw if the load is not successful.
|
|
23
|
+
*/
|
|
24
|
+
export class EnforcingKnexEntityLoader<
|
|
25
|
+
TFields extends Record<string, any>,
|
|
26
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
27
|
+
TViewerContext extends ViewerContext,
|
|
28
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
29
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
30
|
+
TFields,
|
|
31
|
+
TIDField,
|
|
32
|
+
TViewerContext,
|
|
33
|
+
TEntity,
|
|
34
|
+
TSelectedFields
|
|
35
|
+
>,
|
|
36
|
+
TSelectedFields extends keyof TFields,
|
|
37
|
+
> {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly knexEntityLoader: AuthorizationResultBasedKnexEntityLoader<
|
|
40
|
+
TFields,
|
|
41
|
+
TIDField,
|
|
42
|
+
TViewerContext,
|
|
43
|
+
TEntity,
|
|
44
|
+
TPrivacyPolicy,
|
|
45
|
+
TSelectedFields
|
|
46
|
+
>,
|
|
47
|
+
private readonly queryContext: EntityQueryContext,
|
|
48
|
+
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
|
|
49
|
+
protected readonly metricsAdapter: IEntityMetricsAdapter,
|
|
50
|
+
private readonly constructionUtils: EntityConstructionUtils<
|
|
51
|
+
TFields,
|
|
52
|
+
TIDField,
|
|
53
|
+
TViewerContext,
|
|
54
|
+
TEntity,
|
|
55
|
+
TPrivacyPolicy,
|
|
56
|
+
TSelectedFields
|
|
57
|
+
>,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load the first entity matching the conjunction of field equality operands and
|
|
62
|
+
* query modifiers.
|
|
63
|
+
*
|
|
64
|
+
* This is a convenience method for {@link loadManyByFieldEqualityConjunctionAsync}. However, the
|
|
65
|
+
* orderBy query modifier is required to ensure consistent results if more than one entity matches
|
|
66
|
+
* the filters.
|
|
67
|
+
*
|
|
68
|
+
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
|
|
69
|
+
* @returns the first entity matching the filters, or null if none match
|
|
70
|
+
*/
|
|
71
|
+
async loadFirstByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
|
72
|
+
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
|
73
|
+
querySelectionModifiers: Omit<
|
|
74
|
+
EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
|
75
|
+
'limit'
|
|
76
|
+
> &
|
|
77
|
+
Required<Pick<EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>, 'orderBy'>>,
|
|
78
|
+
): Promise<TEntity | null> {
|
|
79
|
+
const entityResult = await this.knexEntityLoader.loadFirstByFieldEqualityConjunctionAsync(
|
|
80
|
+
fieldEqualityOperands,
|
|
81
|
+
querySelectionModifiers,
|
|
82
|
+
);
|
|
83
|
+
return entityResult?.enforceValue() ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Load entities matching the conjunction of field equality operands and
|
|
88
|
+
* query modifiers.
|
|
89
|
+
*
|
|
90
|
+
* Typically this is used for complex queries that cannot be expressed through simpler
|
|
91
|
+
* convenience methods such as {@link loadManyByFieldEqualingAsync}.
|
|
92
|
+
*
|
|
93
|
+
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
|
|
94
|
+
* @returns entities matching the filters
|
|
95
|
+
*/
|
|
96
|
+
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
|
97
|
+
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
|
98
|
+
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
|
99
|
+
): Promise<readonly TEntity[]> {
|
|
100
|
+
const entityResults = await this.knexEntityLoader.loadManyByFieldEqualityConjunctionAsync(
|
|
101
|
+
fieldEqualityOperands,
|
|
102
|
+
querySelectionModifiers,
|
|
103
|
+
);
|
|
104
|
+
return entityResults.map((result) => result.enforceValue());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load entities with a raw SQL WHERE clause.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* Load entities with SQL function
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const entitiesWithJsonKey = await ExampleEntity.loader(vc)
|
|
114
|
+
* .loadManyByRawWhereClauseAsync(
|
|
115
|
+
* "json_column->>'key_name' = ?",
|
|
116
|
+
* ['value'],
|
|
117
|
+
* );
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* Load entities with tuple matching
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const entities = await ExampleEntity.loader(vc)
|
|
124
|
+
* .loadManyByRawWhereClauseAsync(
|
|
125
|
+
* '(column_1, column_2) IN ((?, ?), (?, ?))',
|
|
126
|
+
* [value1, value2, value3, value4],
|
|
127
|
+
* );
|
|
128
|
+
* ```
|
|
129
|
+
* @param rawWhereClause - SQL WHERE clause. Interpolated values should be specified as ?-placeholders or :key_name
|
|
130
|
+
* @param bindings - values to bind to the placeholders in the WHERE clause
|
|
131
|
+
* @param querySelectionModifiers - limit, offset, and orderBy for the query.
|
|
132
|
+
* @returns entities matching the WHERE clause
|
|
133
|
+
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
|
134
|
+
* @throws Error when rawWhereClause or bindings are invalid
|
|
135
|
+
*
|
|
136
|
+
* @deprecated Use loadManyBySQL instead for safer value bindings and more flexible query building.
|
|
137
|
+
*/
|
|
138
|
+
async loadManyByRawWhereClauseAsync(
|
|
139
|
+
rawWhereClause: string,
|
|
140
|
+
bindings: any[] | object,
|
|
141
|
+
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
|
142
|
+
): Promise<readonly TEntity[]> {
|
|
143
|
+
const entityResults = await this.knexEntityLoader.loadManyByRawWhereClauseAsync(
|
|
144
|
+
rawWhereClause,
|
|
145
|
+
bindings,
|
|
146
|
+
querySelectionModifiers,
|
|
147
|
+
);
|
|
148
|
+
return entityResults.map((result) => result.enforceValue());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const entities = await ExampleEntity.loader(vc)
|
|
157
|
+
* .loadManyBySQL(sql`age >= ${18} AND status = ${'active'}`)
|
|
158
|
+
* .orderBy('createdAt', 'DESC')
|
|
159
|
+
* .limit(10)
|
|
160
|
+
* .executeAsync();
|
|
161
|
+
*
|
|
162
|
+
* const { between, inArray } = SQLFragmentHelpers;
|
|
163
|
+
* const filtered = await ExampleEntity.loader(vc)
|
|
164
|
+
* .loadManyBySQL(
|
|
165
|
+
* sql`${between('age', 18, 65)} AND ${inArray('role', ['admin', 'moderator'])}`
|
|
166
|
+
* )
|
|
167
|
+
* .executeAsync();
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
loadManyBySQL(
|
|
171
|
+
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
|
172
|
+
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
|
173
|
+
): EnforcingSQLQueryBuilder<
|
|
174
|
+
TFields,
|
|
175
|
+
TIDField,
|
|
176
|
+
TViewerContext,
|
|
177
|
+
TEntity,
|
|
178
|
+
TPrivacyPolicy,
|
|
179
|
+
TSelectedFields
|
|
180
|
+
> {
|
|
181
|
+
return new EnforcingSQLQueryBuilder(this.knexEntityLoader, fragment, modifiers);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load a page of entities with Relay-style cursor pagination.
|
|
186
|
+
*
|
|
187
|
+
* @param args - Pagination arguments with pagination and either first/after or last/before
|
|
188
|
+
* @returns a page of entities matching the pagination arguments
|
|
189
|
+
* @throws EntityNotAuthorizedError if viewer is not authorized to view any returned entity
|
|
190
|
+
*/
|
|
191
|
+
async loadPageAsync(
|
|
192
|
+
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
|
|
193
|
+
): Promise<Connection<TEntity>> {
|
|
194
|
+
const pageResult = await this.knexDataManager.loadPageAsync(this.queryContext, args);
|
|
195
|
+
const edges = await Promise.all(
|
|
196
|
+
pageResult.edges.map(async (edge) => {
|
|
197
|
+
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
|
|
198
|
+
edge.node,
|
|
199
|
+
);
|
|
200
|
+
const entity = entityResult.enforceValue();
|
|
201
|
+
return {
|
|
202
|
+
...edge,
|
|
203
|
+
node: entity,
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
edges,
|
|
210
|
+
pageInfo: pageResult.pageInfo,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get cursor for a given entity that matches what loadPageAsync would produce.
|
|
216
|
+
* Useful for constructing pagination cursors for entities returned from other loader methods that can then be passed to loadPageAsync for pagination.
|
|
217
|
+
* Most commonly used for testing pagination behavior.
|
|
218
|
+
*
|
|
219
|
+
* @param entity - The entity to get the pagination cursor for.
|
|
220
|
+
* @returns The pagination cursor for the given entity.
|
|
221
|
+
*/
|
|
222
|
+
getPaginationCursorForEntity(entity: TEntity): string {
|
|
223
|
+
return this.knexEntityLoader.getPaginationCursorForEntity(entity);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* SQL query builder for EnforcingKnexEntityLoader.
|
|
229
|
+
* Provides a fluent API for building and executing SQL queries with enforced authorization.
|
|
230
|
+
*/
|
|
231
|
+
export class EnforcingSQLQueryBuilder<
|
|
232
|
+
TFields extends Record<string, any>,
|
|
233
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
234
|
+
TViewerContext extends ViewerContext,
|
|
235
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
236
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
237
|
+
TFields,
|
|
238
|
+
TIDField,
|
|
239
|
+
TViewerContext,
|
|
240
|
+
TEntity,
|
|
241
|
+
TSelectedFields
|
|
242
|
+
>,
|
|
243
|
+
TSelectedFields extends keyof TFields,
|
|
244
|
+
> extends BaseSQLQueryBuilder<TFields, TSelectedFields, TEntity> {
|
|
245
|
+
constructor(
|
|
246
|
+
private readonly knexEntityLoader: AuthorizationResultBasedKnexEntityLoader<
|
|
247
|
+
TFields,
|
|
248
|
+
TIDField,
|
|
249
|
+
TViewerContext,
|
|
250
|
+
TEntity,
|
|
251
|
+
TPrivacyPolicy,
|
|
252
|
+
TSelectedFields
|
|
253
|
+
>,
|
|
254
|
+
sqlFragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
|
255
|
+
modifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>,
|
|
256
|
+
) {
|
|
257
|
+
super(sqlFragment, modifiers);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Execute the query.
|
|
262
|
+
* @returns entities matching the query
|
|
263
|
+
* @throws EntityNotAuthorizedError if viewer is not authorized to view any entity
|
|
264
|
+
*/
|
|
265
|
+
async executeInternalAsync(): Promise<readonly TEntity[]> {
|
|
266
|
+
const entityResults = await this.knexEntityLoader
|
|
267
|
+
.loadManyBySQL(this.getSQLFragment(), this.getModifiers())
|
|
268
|
+
.executeAsync();
|
|
269
|
+
return entityResults.map((result) => result.enforceValue());
|
|
270
|
+
}
|
|
271
|
+
}
|