@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 CHANGED
@@ -39,4 +39,4 @@ export const createDefaultEntityCompanionProvider = (
39
39
  }
40
40
  );
41
41
  };
42
- ```
42
+ ```
@@ -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 | undefined): this;
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 | undefined): this;
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 = undefined) {
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 = undefined) {
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
- async fetchManyByFieldEqualityConjunctionInternalAsync(queryInterface, tableName, tableFieldSingleValueEqualityOperands, tableFieldMultiValueEqualityOperands, querySelectionModifiers) {
104
- let query = queryInterface.select().from(tableName);
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
- query = query.where(whereObject);
113
+ result = result.where(whereObject);
114
114
  }
115
115
  if (nullTableFieldSingleValueEqualityOperands.length > 0) {
116
116
  for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
117
- query = query.whereNull(tableField);
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
- query = query.where((builder) => {
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 | undefined | readonly SupportedSQLValue[] | Readonly<{
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 SupportedSQLValue[];
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 | undefined ? K : never;
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`/`undefined` to `IS 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 | undefined): SQLFragment<TFields>;
184
+ eq(value: TValue | null): SQLFragment<TFields>;
184
185
  /**
185
186
  * Generates an inequality condition (`!= value`).
186
- * Automatically converts `null`/`undefined` to `IS NOT 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 | undefined): SQLFragment<TFields>;
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 | undefined : SupportedSQLValue;
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`/`undefined` to `IS 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`/`undefined` to `IS 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`/`undefined` to `IS NOT 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`/`undefined` to `IS NOT 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/undefined to IS 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/undefined to IS NOT NULL.
812
+ * Inequality operator. Automatically converts null to IS NOT NULL.
812
813
  */
813
814
  neq: typeof neqHelper;
814
815
  /**
@@ -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 and undefined
102
- if (value === null || value === undefined) {
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`/`undefined` to `IS 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 || value === undefined) {
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`/`undefined` to `IS NOT 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 || value === undefined) {
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/undefined to IS NULL.
723
+ * Equality operator. Automatically converts null to IS NULL.
721
724
  */
722
725
  eq: eqHelper,
723
726
  /**
724
- * Inequality operator. Automatically converts null/undefined to IS NOT 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
- const conditions = [baseWhere, cursorCondition].filter((it) => !!it);
214
- if (conditions.length === 0) {
215
- return sql `TRUE`;
219
+ if (!baseWhere) {
220
+ return cursorCondition ?? sql `TRUE`;
216
221
  }
217
- if (conditions.length === 1) {
218
- return conditions[0];
222
+ if (!cursorCondition) {
223
+ return baseWhere;
219
224
  }
220
- // Wrap baseWhere in parens if combining with cursor condition
221
- // We know we have exactly 2 conditions at this point
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.63.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 eslint src",
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.63.0",
33
- "knex": "^3.1.0"
29
+ "@expo/entity": "^0.64.0",
30
+ "knex": "^3.2.9"
34
31
  },
35
32
  "devDependencies": {
36
- "@expo/entity-testing-utils": "^0.63.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": "5.9.3"
37
+ "typescript": "6.0.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
41
  },
42
- "gitHead": "dbd1cc847952754acc0eb407165cbb7b8350d2df"
42
+ "gitHead": "3f10a9e70eab45ae95acdae133055d8a3ec04ce8"
43
43
  }