@expo/entity-database-adapter-knex 0.63.0 → 0.64.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/README.md +1 -1
- package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +18 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +28 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +19 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +37 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +2 -2
- package/build/src/BaseSQLQueryBuilder.js +2 -2
- package/build/src/EnforcingKnexEntityLoader.d.ts +18 -0
- package/build/src/EnforcingKnexEntityLoader.js +22 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +4 -0
- package/build/src/PostgresEntityDatabaseAdapter.js +24 -10
- package/build/src/SQLOperator.d.ts +15 -14
- package/build/src/SQLOperator.js +11 -8
- package/build/src/internal/EntityKnexDataManager.d.ts +2 -0
- package/build/src/internal/EntityKnexDataManager.js +13 -10
- package/package.json +16 -16
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +36 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +67 -0
- package/src/BaseSQLQueryBuilder.ts +2 -2
- package/src/EnforcingKnexEntityLoader.ts +26 -0
- package/src/PostgresEntityDatabaseAdapter.ts +68 -20
- package/src/SQLOperator.ts +23 -22
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +97 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +17 -0
- package/src/__tests__/SQLOperator-test.ts +2 -29
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +25 -1
- package/src/internal/EntityKnexDataManager.ts +38 -9
package/README.md
CHANGED
|
@@ -235,6 +235,24 @@ export declare class AuthorizationResultBasedKnexEntityLoader<TFields extends Re
|
|
|
235
235
|
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
|
236
236
|
*/
|
|
237
237
|
loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[], querySelectionModifiers?: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>): Promise<readonly Result<TEntity>[]>;
|
|
238
|
+
/**
|
|
239
|
+
* Count entities matching the conjunction of field equality operands.
|
|
240
|
+
* This does not perform authorization since count does not load full entities.
|
|
241
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
242
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
243
|
+
*
|
|
244
|
+
* @returns count of entities matching the filters
|
|
245
|
+
*/
|
|
246
|
+
countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[]): Promise<number>;
|
|
247
|
+
/**
|
|
248
|
+
* Count entities matching a SQL fragment.
|
|
249
|
+
* This does not perform authorization since count does not load full entities.
|
|
250
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
251
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
252
|
+
*
|
|
253
|
+
* @returns count of entities matching the query
|
|
254
|
+
*/
|
|
255
|
+
countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number>;
|
|
238
256
|
/**
|
|
239
257
|
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
|
240
258
|
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.
|
|
@@ -43,6 +43,34 @@ export class AuthorizationResultBasedKnexEntityLoader {
|
|
|
43
43
|
const fieldObjects = await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(this.queryContext, fieldEqualityOperands, querySelectionModifiers);
|
|
44
44
|
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Count entities matching the conjunction of field equality operands.
|
|
48
|
+
* This does not perform authorization since count does not load full entities.
|
|
49
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
50
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
51
|
+
*
|
|
52
|
+
* @returns count of entities matching the filters
|
|
53
|
+
*/
|
|
54
|
+
async countByFieldEqualityConjunctionAsync(fieldEqualityOperands) {
|
|
55
|
+
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
|
56
|
+
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
|
57
|
+
? [fieldEqualityOperand.fieldValue]
|
|
58
|
+
: fieldEqualityOperand.fieldValues;
|
|
59
|
+
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
|
|
60
|
+
}
|
|
61
|
+
return await this.knexDataManager.countByFieldEqualityConjunctionAsync(this.queryContext, fieldEqualityOperands);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Count entities matching a SQL fragment.
|
|
65
|
+
* This does not perform authorization since count does not load full entities.
|
|
66
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
67
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
68
|
+
*
|
|
69
|
+
* @returns count of entities matching the query
|
|
70
|
+
*/
|
|
71
|
+
async countBySQLAsync(fragment) {
|
|
72
|
+
return await this.knexDataManager.countBySQLFragmentAsync(this.queryContext, fragment);
|
|
73
|
+
}
|
|
46
74
|
/**
|
|
47
75
|
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
|
48
76
|
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.
|
|
@@ -136,5 +136,24 @@ export declare abstract class BasePostgresEntityDatabaseAdapter<TFields extends
|
|
|
136
136
|
*/
|
|
137
137
|
fetchManyBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>, querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
138
138
|
protected abstract fetchManyBySQLFragmentInternalAsync(queryInterface: Knex, tableName: string, sqlFragment: SQLFragment<TFields>, querySelectionModifiers: TableQuerySelectionModifiers<TFields>): Promise<object[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Count objects matching the conjunction of where clauses constructed from
|
|
141
|
+
* specified field equality operands.
|
|
142
|
+
*
|
|
143
|
+
* @param queryContext - query context with which to perform the count
|
|
144
|
+
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
|
145
|
+
* @returns count of objects matching the query
|
|
146
|
+
*/
|
|
147
|
+
countByFieldEqualityConjunctionAsync<N extends keyof TFields>(queryContext: EntityQueryContext, fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[]): Promise<number>;
|
|
148
|
+
protected abstract countByFieldEqualityConjunctionInternalAsync(queryInterface: Knex, tableName: string, tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[]): Promise<number>;
|
|
149
|
+
/**
|
|
150
|
+
* Count objects matching the SQL fragment.
|
|
151
|
+
*
|
|
152
|
+
* @param queryContext - query context with which to perform the count
|
|
153
|
+
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
154
|
+
* @returns count of objects matching the query
|
|
155
|
+
*/
|
|
156
|
+
countBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>): Promise<number>;
|
|
157
|
+
protected abstract countBySQLFragmentInternalAsync(queryInterface: Knex, tableName: string, sqlFragment: SQLFragment<TFields>): Promise<number>;
|
|
139
158
|
private convertToTableQueryModifiers;
|
|
140
159
|
}
|
|
@@ -72,6 +72,43 @@ export class BasePostgresEntityDatabaseAdapter extends EntityDatabaseAdapter {
|
|
|
72
72
|
const results = await this.fetchManyBySQLFragmentInternalAsync(queryContext.getQueryInterface(), this.entityConfiguration.tableName, sqlFragment, this.convertToTableQueryModifiers(querySelectionModifiers));
|
|
73
73
|
return results.map((result) => transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result));
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Count objects matching the conjunction of where clauses constructed from
|
|
77
|
+
* specified field equality operands.
|
|
78
|
+
*
|
|
79
|
+
* @param queryContext - query context with which to perform the count
|
|
80
|
+
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
|
81
|
+
* @returns count of objects matching the query
|
|
82
|
+
*/
|
|
83
|
+
async countByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands) {
|
|
84
|
+
const tableFieldSingleValueOperands = [];
|
|
85
|
+
const tableFieldMultipleValueOperands = [];
|
|
86
|
+
for (const operand of fieldEqualityOperands) {
|
|
87
|
+
if (isSingleValueFieldEqualityCondition(operand)) {
|
|
88
|
+
tableFieldSingleValueOperands.push({
|
|
89
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
90
|
+
tableValue: operand.fieldValue,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
tableFieldMultipleValueOperands.push({
|
|
95
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
96
|
+
tableValues: operand.fieldValues,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return await this.countByFieldEqualityConjunctionInternalAsync(queryContext.getQueryInterface(), this.entityConfiguration.tableName, tableFieldSingleValueOperands, tableFieldMultipleValueOperands);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Count objects matching the SQL fragment.
|
|
104
|
+
*
|
|
105
|
+
* @param queryContext - query context with which to perform the count
|
|
106
|
+
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
107
|
+
* @returns count of objects matching the query
|
|
108
|
+
*/
|
|
109
|
+
async countBySQLFragmentAsync(queryContext, sqlFragment) {
|
|
110
|
+
return await this.countBySQLFragmentInternalAsync(queryContext.getQueryInterface(), this.entityConfiguration.tableName, sqlFragment);
|
|
111
|
+
}
|
|
75
112
|
convertToTableQueryModifiers(querySelectionModifiers) {
|
|
76
113
|
const orderBy = querySelectionModifiers.orderBy;
|
|
77
114
|
return {
|
|
@@ -25,7 +25,7 @@ export declare abstract class BaseSQLQueryBuilder<TFields extends Record<string,
|
|
|
25
25
|
/**
|
|
26
26
|
* Order by a field. Can be called multiple times to add multiple order bys.
|
|
27
27
|
*/
|
|
28
|
-
orderBy(fieldName: TSelectedFields, order?: OrderByOrdering, nulls?: NullsOrdering
|
|
28
|
+
orderBy(fieldName: TSelectedFields, order?: OrderByOrdering, nulls?: NullsOrdering): this;
|
|
29
29
|
/**
|
|
30
30
|
* Order by a SQL fragment expression.
|
|
31
31
|
* Provides type-safe, parameterized ORDER BY clauses
|
|
@@ -44,7 +44,7 @@ export declare abstract class BaseSQLQueryBuilder<TFields extends Record<string,
|
|
|
44
44
|
* @param fragment - The SQL fragment to order by. Must not include the ASC/DESC keyword, as ordering direction is determined by the `order` parameter.
|
|
45
45
|
* @param order - The ordering direction (ascending or descending). Defaults to ascending.
|
|
46
46
|
*/
|
|
47
|
-
orderBySQL(fragment: SQLFragment<Pick<TFields, TSelectedFields>>, order?: OrderByOrdering, nulls?: NullsOrdering
|
|
47
|
+
orderBySQL(fragment: SQLFragment<Pick<TFields, TSelectedFields>>, order?: OrderByOrdering, nulls?: NullsOrdering): this;
|
|
48
48
|
/**
|
|
49
49
|
* Get the current modifiers as QuerySelectionModifiersWithOrderByFragment<TFields>
|
|
50
50
|
*/
|
|
@@ -27,7 +27,7 @@ export class BaseSQLQueryBuilder {
|
|
|
27
27
|
/**
|
|
28
28
|
* Order by a field. Can be called multiple times to add multiple order bys.
|
|
29
29
|
*/
|
|
30
|
-
orderBy(fieldName, order = OrderByOrdering.ASCENDING, nulls
|
|
30
|
+
orderBy(fieldName, order = OrderByOrdering.ASCENDING, nulls) {
|
|
31
31
|
this.modifiers.orderBy = [...(this.modifiers.orderBy ?? []), { fieldName, order, nulls }];
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
@@ -49,7 +49,7 @@ export class BaseSQLQueryBuilder {
|
|
|
49
49
|
* @param fragment - The SQL fragment to order by. Must not include the ASC/DESC keyword, as ordering direction is determined by the `order` parameter.
|
|
50
50
|
* @param order - The ordering direction (ascending or descending). Defaults to ascending.
|
|
51
51
|
*/
|
|
52
|
-
orderBySQL(fragment, order = OrderByOrdering.ASCENDING, nulls
|
|
52
|
+
orderBySQL(fragment, order = OrderByOrdering.ASCENDING, nulls) {
|
|
53
53
|
this.modifiers.orderBy = [
|
|
54
54
|
...(this.modifiers.orderBy ?? []),
|
|
55
55
|
{ fieldFragment: fragment, order, nulls },
|
|
@@ -38,6 +38,24 @@ export declare class EnforcingKnexEntityLoader<TFields extends Record<string, an
|
|
|
38
38
|
* @returns entities matching the filters
|
|
39
39
|
*/
|
|
40
40
|
loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(fieldEqualityOperands: FieldEqualityCondition<TFields, N>[], querySelectionModifiers?: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields>): Promise<readonly TEntity[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Count entities matching the conjunction of field equality operands.
|
|
43
|
+
* This does not perform authorization since count does not load full entities.
|
|
44
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
45
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
46
|
+
*
|
|
47
|
+
* @returns count of entities matching the filters
|
|
48
|
+
*/
|
|
49
|
+
countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(fieldEqualityOperands: FieldEqualityCondition<TFields, N>[]): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Count entities matching a SQL fragment.
|
|
52
|
+
* This does not perform authorization since count does not load full entity rows.
|
|
53
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
54
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
55
|
+
*
|
|
56
|
+
* @returns count of entities matching the query
|
|
57
|
+
*/
|
|
58
|
+
countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number>;
|
|
41
59
|
/**
|
|
42
60
|
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
|
|
43
61
|
*
|
|
@@ -45,6 +45,28 @@ export class EnforcingKnexEntityLoader {
|
|
|
45
45
|
const entityResults = await this.knexEntityLoader.loadManyByFieldEqualityConjunctionAsync(fieldEqualityOperands, querySelectionModifiers);
|
|
46
46
|
return entityResults.map((result) => result.enforceValue());
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Count entities matching the conjunction of field equality operands.
|
|
50
|
+
* This does not perform authorization since count does not load full entities.
|
|
51
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
52
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
53
|
+
*
|
|
54
|
+
* @returns count of entities matching the filters
|
|
55
|
+
*/
|
|
56
|
+
async countByFieldEqualityConjunctionAsync(fieldEqualityOperands) {
|
|
57
|
+
return await this.knexEntityLoader.countByFieldEqualityConjunctionAsync(fieldEqualityOperands);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Count entities matching a SQL fragment.
|
|
61
|
+
* This does not perform authorization since count does not load full entity rows.
|
|
62
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
63
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
64
|
+
*
|
|
65
|
+
* @returns count of entities matching the query
|
|
66
|
+
*/
|
|
67
|
+
async countBySQLAsync(fragment) {
|
|
68
|
+
return await this.knexEntityLoader.countBySQLAsync(fragment);
|
|
69
|
+
}
|
|
48
70
|
/**
|
|
49
71
|
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
|
|
50
72
|
*
|
|
@@ -12,8 +12,12 @@ export declare class PostgresEntityDatabaseAdapter<TFields extends Record<string
|
|
|
12
12
|
protected fetchManyWhereInternalAsync(queryInterface: Knex, tableName: string, tableColumns: readonly string[], tableTuples: any[][]): Promise<object[]>;
|
|
13
13
|
protected fetchOneWhereInternalAsync(queryInterface: Knex, tableName: string, tableColumns: readonly string[], tableTuple: readonly any[]): Promise<object | null>;
|
|
14
14
|
private applyQueryModifiersToQuery;
|
|
15
|
+
private applyFieldEqualityConjunctionWhereClause;
|
|
15
16
|
protected fetchManyByFieldEqualityConjunctionInternalAsync(queryInterface: Knex, tableName: string, tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[], querySelectionModifiers: TableQuerySelectionModifiers<TFields>): Promise<object[]>;
|
|
17
|
+
private applySQLFragmentWhereClause;
|
|
16
18
|
protected fetchManyBySQLFragmentInternalAsync(queryInterface: Knex, tableName: string, sqlFragment: SQLFragment<TFields>, querySelectionModifiers: TableQuerySelectionModifiers<TFields>): Promise<object[]>;
|
|
19
|
+
protected countByFieldEqualityConjunctionInternalAsync(queryInterface: Knex, tableName: string, tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[], tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[]): Promise<number>;
|
|
20
|
+
protected countBySQLFragmentInternalAsync(queryInterface: Knex, tableName: string, sqlFragment: SQLFragment<TFields>): Promise<number>;
|
|
17
21
|
protected insertInternalAsync(queryInterface: Knex, tableName: string, object: object): Promise<object[]>;
|
|
18
22
|
protected updateInternalAsync(queryInterface: Knex, tableName: string, tableIdField: string, id: any, object: object): Promise<{
|
|
19
23
|
updatedRowCount: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDatabaseFieldForEntityField } from '@expo/entity';
|
|
1
|
+
import { getDatabaseFieldForEntityField, RESERVED_ENTITY_COUNT_QUERY_ALIAS } from '@expo/entity';
|
|
2
2
|
import { BasePostgresEntityDatabaseAdapter, NullsOrdering, OrderByOrdering, } from "./BasePostgresEntityDatabaseAdapter.js";
|
|
3
3
|
import { JSONArrayField, MaybeJSONArrayField } from "./EntityFields.js";
|
|
4
4
|
import { wrapNativePostgresCallAsync } from "./errors/wrapNativePostgresCallAsync.js";
|
|
@@ -100,8 +100,8 @@ export class PostgresEntityDatabaseAdapter extends BasePostgresEntityDatabaseAda
|
|
|
100
100
|
}
|
|
101
101
|
return ret;
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
let
|
|
103
|
+
applyFieldEqualityConjunctionWhereClause(query, tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands) {
|
|
104
|
+
let result = query;
|
|
105
105
|
if (tableFieldSingleValueEqualityOperands.length > 0) {
|
|
106
106
|
const whereObject = {};
|
|
107
107
|
const nonNullTableFieldSingleValueEqualityOperands = tableFieldSingleValueEqualityOperands.filter(({ tableValue }) => tableValue !== null);
|
|
@@ -110,18 +110,18 @@ export class PostgresEntityDatabaseAdapter extends BasePostgresEntityDatabaseAda
|
|
|
110
110
|
for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) {
|
|
111
111
|
whereObject[tableField] = tableValue;
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
result = result.where(whereObject);
|
|
114
114
|
}
|
|
115
115
|
if (nullTableFieldSingleValueEqualityOperands.length > 0) {
|
|
116
116
|
for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
|
|
117
|
-
|
|
117
|
+
result = result.whereNull(tableField);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
if (tableFieldMultiValueEqualityOperands.length > 0) {
|
|
122
122
|
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
|
|
123
123
|
const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null);
|
|
124
|
-
|
|
124
|
+
result = result.where((builder) => {
|
|
125
125
|
builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]);
|
|
126
126
|
// there was at least one null, allow null in this equality clause
|
|
127
127
|
if (nonNullTableValues.length !== tableValues.length) {
|
|
@@ -130,17 +130,31 @@ export class PostgresEntityDatabaseAdapter extends BasePostgresEntityDatabaseAda
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
async fetchManyByFieldEqualityConjunctionInternalAsync(queryInterface, tableName, tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands, querySelectionModifiers) {
|
|
136
|
+
let query = this.applyFieldEqualityConjunctionWhereClause(queryInterface.select().from(tableName), tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands);
|
|
133
137
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
134
138
|
return await wrapNativePostgresCallAsync(() => query);
|
|
135
139
|
}
|
|
140
|
+
applySQLFragmentWhereClause(query, sqlFragment) {
|
|
141
|
+
return query.whereRaw(sqlFragment.sql, sqlFragment.getKnexBindings((fieldName) => getDatabaseFieldForEntityField(this.entityConfiguration, fieldName)));
|
|
142
|
+
}
|
|
136
143
|
async fetchManyBySQLFragmentInternalAsync(queryInterface, tableName, sqlFragment, querySelectionModifiers) {
|
|
137
|
-
let query = queryInterface
|
|
138
|
-
.select()
|
|
139
|
-
.from(tableName)
|
|
140
|
-
.whereRaw(sqlFragment.sql, sqlFragment.getKnexBindings((fieldName) => getDatabaseFieldForEntityField(this.entityConfiguration, fieldName)));
|
|
144
|
+
let query = this.applySQLFragmentWhereClause(queryInterface.select().from(tableName), sqlFragment);
|
|
141
145
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
142
146
|
return await wrapNativePostgresCallAsync(() => query);
|
|
143
147
|
}
|
|
148
|
+
async countByFieldEqualityConjunctionInternalAsync(queryInterface, tableName, tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands) {
|
|
149
|
+
const query = this.applyFieldEqualityConjunctionWhereClause(queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName), tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands);
|
|
150
|
+
const result = await wrapNativePostgresCallAsync(() => query);
|
|
151
|
+
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
|
152
|
+
}
|
|
153
|
+
async countBySQLFragmentInternalAsync(queryInterface, tableName, sqlFragment) {
|
|
154
|
+
const query = this.applySQLFragmentWhereClause(queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName), sqlFragment);
|
|
155
|
+
const result = await wrapNativePostgresCallAsync(() => query);
|
|
156
|
+
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
|
157
|
+
}
|
|
144
158
|
async insertInternalAsync(queryInterface, tableName, object) {
|
|
145
159
|
return await wrapNativePostgresCallAsync(() => queryInterface.insert(object).into(tableName).returning('*'));
|
|
146
160
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
1
2
|
/**
|
|
2
3
|
* Supported SQL value types that can be safely parameterized.
|
|
3
4
|
* This ensures type safety and prevents passing unsupported types to SQL queries.
|
|
4
5
|
*/
|
|
5
|
-
export type SupportedSQLValue = string | number | boolean | null | Date | Buffer | bigint |
|
|
6
|
+
export type SupportedSQLValue = string | number | boolean | null | Date | Buffer | bigint | readonly SupportedSQLValue[] | Readonly<{
|
|
6
7
|
[key: string]: unknown;
|
|
7
8
|
}>;
|
|
8
9
|
/**
|
|
@@ -31,7 +32,7 @@ export declare class SQLFragment<TFields extends Record<string, any>> {
|
|
|
31
32
|
*
|
|
32
33
|
* @param getColumnForField - function that resolves an entity field name to its database column name
|
|
33
34
|
*/
|
|
34
|
-
getKnexBindings(getColumnForField: (fieldName: keyof TFields) => string): readonly
|
|
35
|
+
getKnexBindings(getColumnForField: (fieldName: keyof TFields) => string): readonly Knex.RawBinding[];
|
|
35
36
|
/**
|
|
36
37
|
* Combine SQL fragments
|
|
37
38
|
*/
|
|
@@ -161,7 +162,7 @@ type PickSupportedSQLValueKeys<T> = {
|
|
|
161
162
|
[K in keyof T]: T[K] extends SupportedSQLValue ? K : never;
|
|
162
163
|
}[keyof T];
|
|
163
164
|
type PickStringValueKeys<T> = {
|
|
164
|
-
[K in keyof T]: T[K] extends string | null
|
|
165
|
+
[K in keyof T]: T[K] extends string | null ? K : never;
|
|
165
166
|
}[keyof T];
|
|
166
167
|
type JsonSerializable = string | number | boolean | null | undefined | readonly JsonSerializable[] | {
|
|
167
168
|
readonly [key: string]: JsonSerializable;
|
|
@@ -175,20 +176,20 @@ type JsonSerializable = string | number | boolean | null | undefined | readonly
|
|
|
175
176
|
export declare class SQLChainableFragment<TFields extends Record<string, any>, TValue extends SupportedSQLValue> extends SQLFragment<TFields> {
|
|
176
177
|
/**
|
|
177
178
|
* Generates an equality condition (`= value`).
|
|
178
|
-
* Automatically converts `null
|
|
179
|
+
* Automatically converts `null` to `IS NULL`.
|
|
179
180
|
*
|
|
180
181
|
* @param value - The value to compare against
|
|
181
182
|
* @returns A {@link SQLFragment} representing the equality condition
|
|
182
183
|
*/
|
|
183
|
-
eq(value: TValue | null
|
|
184
|
+
eq(value: TValue | null): SQLFragment<TFields>;
|
|
184
185
|
/**
|
|
185
186
|
* Generates an inequality condition (`!= value`).
|
|
186
|
-
* Automatically converts `null
|
|
187
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
187
188
|
*
|
|
188
189
|
* @param value - The value to compare against
|
|
189
190
|
* @returns A {@link SQLFragment} representing the inequality condition
|
|
190
191
|
*/
|
|
191
|
-
neq(value: TValue | null
|
|
192
|
+
neq(value: TValue | null): SQLFragment<TFields>;
|
|
192
193
|
/**
|
|
193
194
|
* Generates a greater-than condition (`> value`).
|
|
194
195
|
*
|
|
@@ -312,7 +313,7 @@ declare const ALLOWED_CAST_TYPES: readonly ["int", "integer", "int2", "int4", "i
|
|
|
312
313
|
*/
|
|
313
314
|
export type PostgresCastType = (typeof ALLOWED_CAST_TYPES)[number];
|
|
314
315
|
type ExtractFragmentFields<T> = T extends SQLFragment<infer F> ? F : never;
|
|
315
|
-
type FragmentValueNullable<TFragment> = TFragment extends SQLChainableFragment<any, infer TValue> ? TValue | null
|
|
316
|
+
type FragmentValueNullable<TFragment> = TFragment extends SQLChainableFragment<any, infer TValue> ? TValue | null : SupportedSQLValue;
|
|
316
317
|
type FragmentValue<TFragment> = TFragment extends SQLChainableFragment<any, infer TValue> ? TValue : SupportedSQLValue;
|
|
317
318
|
type FragmentValueArray<TFragment> = TFragment extends SQLChainableFragment<any, infer TValue> ? readonly TValue[] : readonly SupportedSQLValue[];
|
|
318
319
|
/**
|
|
@@ -477,7 +478,7 @@ declare function isNotNullHelper<TFragment extends SQLFragment<any>>(fragment: T
|
|
|
477
478
|
declare function isNotNullHelper<TFields extends Record<string, any>, N extends keyof TFields>(fieldName: N): SQLFragment<TFields>;
|
|
478
479
|
/**
|
|
479
480
|
* Generates an equality condition (`= value`) from a fragment.
|
|
480
|
-
* Automatically converts `null
|
|
481
|
+
* Automatically converts `null` to `IS NULL`.
|
|
481
482
|
*
|
|
482
483
|
* @param fragment - A SQLFragment or SQLChainableFragment to compare
|
|
483
484
|
* @param value - The value to compare against
|
|
@@ -485,7 +486,7 @@ declare function isNotNullHelper<TFields extends Record<string, any>, N extends
|
|
|
485
486
|
declare function eqHelper<TFragment extends SQLFragment<any>>(fragment: TFragment, value: FragmentValueNullable<TFragment>): SQLFragment<ExtractFragmentFields<TFragment>>;
|
|
486
487
|
/**
|
|
487
488
|
* Generates an equality condition (`= value`) from a field name.
|
|
488
|
-
* Automatically converts `null
|
|
489
|
+
* Automatically converts `null` to `IS NULL`.
|
|
489
490
|
*
|
|
490
491
|
* @param fieldName - The entity field name to compare
|
|
491
492
|
* @param value - The value to compare against
|
|
@@ -493,7 +494,7 @@ declare function eqHelper<TFragment extends SQLFragment<any>>(fragment: TFragmen
|
|
|
493
494
|
declare function eqHelper<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(fieldName: N, value: TFields[N]): SQLFragment<TFields>;
|
|
494
495
|
/**
|
|
495
496
|
* Generates an inequality condition (`!= value`) from a fragment.
|
|
496
|
-
* Automatically converts `null
|
|
497
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
497
498
|
*
|
|
498
499
|
* @param fragment - A SQLFragment or SQLChainableFragment to compare
|
|
499
500
|
* @param value - The value to compare against
|
|
@@ -501,7 +502,7 @@ declare function eqHelper<TFields extends Record<string, any>, N extends PickSup
|
|
|
501
502
|
declare function neqHelper<TFragment extends SQLFragment<any>>(fragment: TFragment, value: FragmentValueNullable<TFragment>): SQLFragment<ExtractFragmentFields<TFragment>>;
|
|
502
503
|
/**
|
|
503
504
|
* Generates an inequality condition (`!= value`) from a field name.
|
|
504
|
-
* Automatically converts `null
|
|
505
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
505
506
|
*
|
|
506
507
|
* @param fieldName - The entity field name to compare
|
|
507
508
|
* @param value - The value to compare against
|
|
@@ -804,11 +805,11 @@ export declare const SQLExpression: {
|
|
|
804
805
|
*/
|
|
805
806
|
isNotNull: typeof isNotNullHelper;
|
|
806
807
|
/**
|
|
807
|
-
* Equality operator. Automatically converts null
|
|
808
|
+
* Equality operator. Automatically converts null to IS NULL.
|
|
808
809
|
*/
|
|
809
810
|
eq: typeof eqHelper;
|
|
810
811
|
/**
|
|
811
|
-
* Inequality operator. Automatically converts null
|
|
812
|
+
* Inequality operator. Automatically converts null to IS NOT NULL.
|
|
812
813
|
*/
|
|
813
814
|
neq: typeof neqHelper;
|
|
814
815
|
/**
|
package/build/src/SQLOperator.js
CHANGED
|
@@ -23,6 +23,9 @@ export class SQLFragment {
|
|
|
23
23
|
case 'identifier':
|
|
24
24
|
return b.name;
|
|
25
25
|
case 'value':
|
|
26
|
+
// Needs a cast since bigint is supported by knex postgres dialect but not all dialects, and thus isn't included
|
|
27
|
+
// in the type. Because we only use the postgres dialect in this adapter, it's safe to allow it here.
|
|
28
|
+
// https://github.com/knex/knex/issues/5013#issuecomment-3368744254
|
|
26
29
|
return b.value;
|
|
27
30
|
}
|
|
28
31
|
});
|
|
@@ -98,8 +101,8 @@ export class SQLFragment {
|
|
|
98
101
|
* Handles all SupportedSQLValue types.
|
|
99
102
|
*/
|
|
100
103
|
static formatDebugValue(value) {
|
|
101
|
-
// Handle null
|
|
102
|
-
if (value === null
|
|
104
|
+
// Handle null
|
|
105
|
+
if (value === null) {
|
|
103
106
|
return 'NULL';
|
|
104
107
|
}
|
|
105
108
|
// Handle primitives
|
|
@@ -299,26 +302,26 @@ export function sql(strings, ...values) {
|
|
|
299
302
|
export class SQLChainableFragment extends SQLFragment {
|
|
300
303
|
/**
|
|
301
304
|
* Generates an equality condition (`= value`).
|
|
302
|
-
* Automatically converts `null
|
|
305
|
+
* Automatically converts `null` to `IS NULL`.
|
|
303
306
|
*
|
|
304
307
|
* @param value - The value to compare against
|
|
305
308
|
* @returns A {@link SQLFragment} representing the equality condition
|
|
306
309
|
*/
|
|
307
310
|
eq(value) {
|
|
308
|
-
if (value === null
|
|
311
|
+
if (value === null) {
|
|
309
312
|
return this.isNull();
|
|
310
313
|
}
|
|
311
314
|
return sql `${this} = ${value}`;
|
|
312
315
|
}
|
|
313
316
|
/**
|
|
314
317
|
* Generates an inequality condition (`!= value`).
|
|
315
|
-
* Automatically converts `null
|
|
318
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
316
319
|
*
|
|
317
320
|
* @param value - The value to compare against
|
|
318
321
|
* @returns A {@link SQLFragment} representing the inequality condition
|
|
319
322
|
*/
|
|
320
323
|
neq(value) {
|
|
321
|
-
if (value === null
|
|
324
|
+
if (value === null) {
|
|
322
325
|
return this.isNotNull();
|
|
323
326
|
}
|
|
324
327
|
return sql `${this} != ${value}`;
|
|
@@ -717,11 +720,11 @@ export const SQLExpression = {
|
|
|
717
720
|
*/
|
|
718
721
|
isNotNull: isNotNullHelper,
|
|
719
722
|
/**
|
|
720
|
-
* Equality operator. Automatically converts null
|
|
723
|
+
* Equality operator. Automatically converts null to IS NULL.
|
|
721
724
|
*/
|
|
722
725
|
eq: eqHelper,
|
|
723
726
|
/**
|
|
724
|
-
* Inequality operator. Automatically converts null
|
|
727
|
+
* Inequality operator. Automatically converts null to IS NOT NULL.
|
|
725
728
|
*/
|
|
726
729
|
neq: neqHelper,
|
|
727
730
|
/**
|
|
@@ -88,7 +88,9 @@ export declare class EntityKnexDataManager<TFields extends Record<string, any>,
|
|
|
88
88
|
* @returns array of objects matching the query
|
|
89
89
|
*/
|
|
90
90
|
loadManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(queryContext: EntityQueryContext, fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[], querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
91
|
+
countByFieldEqualityConjunctionAsync<N extends keyof TFields>(queryContext: EntityQueryContext, fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[]): Promise<number>;
|
|
91
92
|
loadManyBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>, querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
93
|
+
countBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>): Promise<number>;
|
|
92
94
|
/**
|
|
93
95
|
* Load a page of objects using cursor-based pagination with unified pagination specification.
|
|
94
96
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDatabaseAdapterPaginationCursorInvalidError, EntityMetricsLoadType, getDatabaseFieldForEntityField, timeAndLogLoadEventAsync, } from '@expo/entity';
|
|
1
|
+
import { EntityDatabaseAdapterPaginationCursorInvalidError, EntityMetricsLoadType, getDatabaseFieldForEntityField, timeAndLogCountEventAsync, timeAndLogLoadEventAsync, } from '@expo/entity';
|
|
2
2
|
import assert from 'assert';
|
|
3
3
|
import { NullsOrdering, OrderByOrdering } from "../BasePostgresEntityDatabaseAdapter.js";
|
|
4
4
|
import { PaginationStrategy } from "../PaginationStrategy.js";
|
|
@@ -42,10 +42,16 @@ export class EntityKnexDataManager {
|
|
|
42
42
|
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
|
|
43
43
|
return await timeAndLogLoadEventAsync(this.metricsAdapter, EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, this.entityClassName, queryContext)(this.databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands, querySelectionModifiers));
|
|
44
44
|
}
|
|
45
|
+
async countByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands) {
|
|
46
|
+
return await timeAndLogCountEventAsync(this.metricsAdapter, EntityMetricsLoadType.COUNT_EQUALITY_CONJUNCTION, this.entityClassName, queryContext)(this.databaseAdapter.countByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands));
|
|
47
|
+
}
|
|
45
48
|
async loadManyBySQLFragmentAsync(queryContext, sqlFragment, querySelectionModifiers) {
|
|
46
49
|
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
|
|
47
50
|
return await timeAndLogLoadEventAsync(this.metricsAdapter, EntityMetricsLoadType.LOAD_MANY_SQL, this.entityClassName, queryContext)(this.databaseAdapter.fetchManyBySQLFragmentAsync(queryContext, sqlFragment, querySelectionModifiers));
|
|
48
51
|
}
|
|
52
|
+
async countBySQLFragmentAsync(queryContext, sqlFragment) {
|
|
53
|
+
return await timeAndLogCountEventAsync(this.metricsAdapter, EntityMetricsLoadType.COUNT_SQL, this.entityClassName, queryContext)(this.databaseAdapter.countBySQLFragmentAsync(queryContext, sqlFragment));
|
|
54
|
+
}
|
|
49
55
|
/**
|
|
50
56
|
* Load a page of objects using cursor-based pagination with unified pagination specification.
|
|
51
57
|
*
|
|
@@ -210,17 +216,14 @@ export class EntityKnexDataManager {
|
|
|
210
216
|
};
|
|
211
217
|
}
|
|
212
218
|
combineWhereConditions(baseWhere, cursorCondition) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return sql `TRUE`;
|
|
219
|
+
if (!baseWhere) {
|
|
220
|
+
return cursorCondition ?? sql `TRUE`;
|
|
216
221
|
}
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
222
|
+
if (!cursorCondition) {
|
|
223
|
+
return baseWhere;
|
|
219
224
|
}
|
|
220
|
-
// Wrap baseWhere in parens
|
|
221
|
-
|
|
222
|
-
const [first, second] = conditions;
|
|
223
|
-
return sql `(${first}) AND ${second}`;
|
|
225
|
+
// Wrap baseWhere in parens when combining with cursor condition
|
|
226
|
+
return sql `(${baseWhere}) AND ${cursorCondition}`;
|
|
224
227
|
}
|
|
225
228
|
augmentOrderByIfNecessary(orderBy, idField) {
|
|
226
229
|
const clauses = orderBy ?? [];
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity-database-adapter-knex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Knex database adapter for @expo/entity",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"entity"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Expo",
|
|
5
10
|
"files": [
|
|
6
11
|
"build",
|
|
7
12
|
"!*.tsbuildinfo",
|
|
8
13
|
"!__*",
|
|
9
14
|
"src"
|
|
10
15
|
],
|
|
16
|
+
"type": "module",
|
|
11
17
|
"main": "build/src/index.js",
|
|
12
18
|
"types": "build/src/index.d.ts",
|
|
13
19
|
"scripts": {
|
|
14
20
|
"build": "tsc --build",
|
|
15
21
|
"prepack": "rm -rf build && yarn build",
|
|
16
22
|
"clean": "yarn build --clean",
|
|
17
|
-
"lint": "yarn run --top-level
|
|
23
|
+
"lint": "yarn run --top-level oxlint --type-aware src",
|
|
18
24
|
"lint-fix": "yarn lint --fix",
|
|
19
25
|
"test": "yarn test:all --rootDir $(pwd)",
|
|
20
26
|
"integration": "yarn integration:all --rootDir $(pwd)"
|
|
21
27
|
},
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=18"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"entity"
|
|
27
|
-
],
|
|
28
|
-
"author": "Expo",
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"type": "module",
|
|
31
28
|
"dependencies": {
|
|
32
|
-
"@expo/entity": "^0.
|
|
33
|
-
"knex": "^3.
|
|
29
|
+
"@expo/entity": "^0.64.0",
|
|
30
|
+
"knex": "^3.2.9"
|
|
34
31
|
},
|
|
35
32
|
"devDependencies": {
|
|
36
|
-
"@expo/entity-testing-utils": "^0.
|
|
33
|
+
"@expo/entity-testing-utils": "^0.64.0",
|
|
37
34
|
"@jest/globals": "30.3.0",
|
|
38
35
|
"pg": "8.20.0",
|
|
39
36
|
"ts-mockito": "2.6.1",
|
|
40
|
-
"typescript": "
|
|
37
|
+
"typescript": "6.0.3"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "3f10a9e70eab45ae95acdae133055d8a3ec04ce8"
|
|
43
43
|
}
|