@expo/entity-database-adapter-knex 0.62.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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +15 -5
  3. package/build/src/AuthorizationResultBasedKnexEntityLoader.js +24 -7
  4. package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +19 -11
  5. package/build/src/BasePostgresEntityDatabaseAdapter.js +37 -13
  6. package/build/src/BaseSQLQueryBuilder.d.ts +2 -2
  7. package/build/src/BaseSQLQueryBuilder.js +2 -2
  8. package/build/src/EnforcingKnexEntityLoader.d.ts +14 -28
  9. package/build/src/EnforcingKnexEntityLoader.js +17 -30
  10. package/build/src/PostgresEntityDatabaseAdapter.d.ts +7 -2
  11. package/build/src/PostgresEntityDatabaseAdapter.js +25 -15
  12. package/build/src/SQLOperator.d.ts +15 -14
  13. package/build/src/SQLOperator.js +11 -8
  14. package/build/src/internal/EntityKnexDataManager.d.ts +2 -10
  15. package/build/src/internal/EntityKnexDataManager.js +12 -22
  16. package/package.json +16 -16
  17. package/src/AuthorizationResultBasedKnexEntityLoader.ts +29 -14
  18. package/src/BasePostgresEntityDatabaseAdapter.ts +60 -29
  19. package/src/BaseSQLQueryBuilder.ts +2 -2
  20. package/src/EnforcingKnexEntityLoader.ts +20 -38
  21. package/src/PostgresEntityDatabaseAdapter.ts +66 -29
  22. package/src/SQLOperator.ts +23 -22
  23. package/src/__integration-tests__/PostgresEntityIntegration-test.ts +140 -116
  24. package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +0 -75
  25. package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +21 -28
  26. package/src/__tests__/EnforcingKnexEntityLoader-test.ts +0 -52
  27. package/src/__tests__/SQLOperator-test.ts +2 -29
  28. package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +26 -12
  29. package/src/internal/EntityKnexDataManager.ts +28 -31
  30. package/src/internal/__tests__/EntityKnexDataManager-test.ts +1 -57
@@ -258,12 +258,11 @@ describe('SQLOperator', () => {
258
258
  });
259
259
 
260
260
  it('handles all SupportedSQLValue types in getDebugString', () => {
261
- const fragment = new SQLFragment('INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
261
+ const fragment = new SQLFragment('INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
262
262
  { type: 'value', value: 'string' },
263
263
  { type: 'value', value: 123 },
264
264
  { type: 'value', value: true },
265
265
  { type: 'value', value: null },
266
- { type: 'value', value: undefined },
267
266
  { type: 'value', value: new Date('2024-01-01T00:00:00.000Z') },
268
267
  { type: 'value', value: Buffer.from('hello') },
269
268
  { type: 'value', value: BigInt(999) },
@@ -272,7 +271,7 @@ describe('SQLOperator', () => {
272
271
 
273
272
  const text = fragment.getDebugString();
274
273
  expect(text).toBe(
275
- "INSERT INTO test VALUES ('string', 123, TRUE, NULL, NULL, '2024-01-01T00:00:00.000Z', '\\x68656c6c6f', 999, ARRAY[1, 2, 3])",
274
+ "INSERT INTO test VALUES ('string', 123, TRUE, NULL, '2024-01-01T00:00:00.000Z', '\\x68656c6c6f', 999, ARRAY[1, 2, 3])",
276
275
  );
277
276
  });
278
277
 
@@ -763,13 +762,6 @@ describe('SQLOperator', () => {
763
762
  expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
764
763
  });
765
764
 
766
- it('handles undefined in equality check', () => {
767
- const fragment = SQLExpression.eq('nullableField', undefined);
768
-
769
- expect(fragment.sql).toBe('?? IS NULL');
770
- expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
771
- });
772
-
773
765
  it('accepts a SQLFragment expression', () => {
774
766
  const fragment = SQLExpression.eq(sql<TestFields>`${entityField('stringField')}`, 'active');
775
767
  expect(fragment.sql).toBe('?? = ?');
@@ -801,13 +793,6 @@ describe('SQLOperator', () => {
801
793
  expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
802
794
  });
803
795
 
804
- it('handles undefined in inequality check', () => {
805
- const fragment = SQLExpression.neq('nullableField', undefined);
806
-
807
- expect(fragment.sql).toBe('?? IS NOT NULL');
808
- expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
809
- });
810
-
811
796
  it('accepts a SQLFragment expression', () => {
812
797
  const fragment = SQLExpression.neq(
813
798
  sql<TestFields>`${entityField('stringField')}`,
@@ -1131,12 +1116,6 @@ describe('SQLOperator', () => {
1131
1116
  expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
1132
1117
  });
1133
1118
 
1134
- it('eq(undefined) uses IS NULL', () => {
1135
- const fragment = makeExpr<string>(stringFieldFragment()).eq(undefined);
1136
- expect(fragment.sql).toBe('?? IS NULL');
1137
- expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
1138
- });
1139
-
1140
1119
  it('neq(value)', () => {
1141
1120
  const fragment = makeExpr<string>(stringFieldFragment()).neq('deleted');
1142
1121
  expect(fragment.sql).toBe('?? != ?');
@@ -1149,12 +1128,6 @@ describe('SQLOperator', () => {
1149
1128
  expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
1150
1129
  });
1151
1130
 
1152
- it('neq(undefined) uses IS NOT NULL', () => {
1153
- const fragment = makeExpr<string>(stringFieldFragment()).neq(undefined);
1154
- expect(fragment.sql).toBe('?? IS NOT NULL');
1155
- expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
1156
- });
1157
-
1158
1131
  it('gt(value)', () => {
1159
1132
  const fragment = makeExpr<number>(intFieldFragment()).gt(10);
1160
1133
  expect(fragment.sql).toBe('?? > ?');
@@ -76,7 +76,7 @@ export class StubPostgresDatabaseAdapter<
76
76
  ): Promise<object[]> {
77
77
  const objectCollection = this.getObjectCollectionForTable(tableName);
78
78
  const results = StubPostgresDatabaseAdapter.uniqBy(tableTuples, (tuple) =>
79
- tuple.join(':'),
79
+ JSON.stringify(tuple),
80
80
  ).reduce(
81
81
  (acc, tableTuple) => {
82
82
  return acc.concat(
@@ -193,23 +193,37 @@ export class StubPostgresDatabaseAdapter<
193
193
  return filteredObjects;
194
194
  }
195
195
 
196
- protected fetchManyByRawWhereClauseInternalAsync(
196
+ protected fetchManyBySQLFragmentInternalAsync(
197
197
  _queryInterface: any,
198
198
  _tableName: string,
199
- _rawWhereClause: string,
200
- _bindings: object | any[],
199
+ _sqlFragment: SQLFragment<TFields>,
201
200
  _querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
202
201
  ): Promise<object[]> {
203
- throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter');
202
+ throw new Error('SQL fragments not supported for StubDatabaseAdapter');
204
203
  }
205
204
 
206
- protected fetchManyBySQLFragmentInternalAsync(
205
+ protected async countByFieldEqualityConjunctionInternalAsync(
206
+ queryInterface: any,
207
+ tableName: string,
208
+ tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
209
+ tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
210
+ ): Promise<number> {
211
+ const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
212
+ queryInterface,
213
+ tableName,
214
+ tableFieldSingleValueEqualityOperands,
215
+ tableFieldMultiValueEqualityOperands,
216
+ { orderBy: undefined, offset: undefined, limit: undefined },
217
+ );
218
+ return results.length;
219
+ }
220
+
221
+ protected countBySQLFragmentInternalAsync(
207
222
  _queryInterface: any,
208
223
  _tableName: string,
209
224
  _sqlFragment: SQLFragment<TFields>,
210
- _querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
211
- ): Promise<object[]> {
212
- throw new Error('SQL fragments not supported for StubDatabaseAdapter');
225
+ ): Promise<number> {
226
+ throw new Error('SQL fragment count not supported for StubDatabaseAdapter');
213
227
  }
214
228
 
215
229
  private generateRandomID(): any {
@@ -254,7 +268,7 @@ export class StubPostgresDatabaseAdapter<
254
268
  tableIdField: string,
255
269
  id: any,
256
270
  object: object,
257
- ): Promise<object[]> {
271
+ ): Promise<{ updatedRowCount: number }> {
258
272
  // SQL does not support empty updates, mirror behavior here for better test simulation
259
273
  if (Object.keys(object).length === 0) {
260
274
  throw new Error(`Empty update (${tableIdField} = ${id})`);
@@ -269,14 +283,14 @@ export class StubPostgresDatabaseAdapter<
269
283
  // SQL updates to a nonexistent row succeed but affect 0 rows,
270
284
  // mirror that behavior here for better test simulation
271
285
  if (objectIndex < 0) {
272
- return [];
286
+ return { updatedRowCount: 0 };
273
287
  }
274
288
 
275
289
  objectCollection[objectIndex] = {
276
290
  ...objectCollection[objectIndex],
277
291
  ...object,
278
292
  };
279
- return [objectCollection[objectIndex]];
293
+ return { updatedRowCount: 1 };
280
294
  }
281
295
 
282
296
  protected async deleteInternalAsync(
@@ -3,6 +3,7 @@ import {
3
3
  EntityDatabaseAdapterPaginationCursorInvalidError,
4
4
  EntityMetricsLoadType,
5
5
  getDatabaseFieldForEntityField,
6
+ timeAndLogCountEventAsync,
6
7
  timeAndLogLoadEventAsync,
7
8
  } from '@expo/entity';
8
9
  import assert from 'assert';
@@ -190,34 +191,19 @@ export class EntityKnexDataManager<
190
191
  );
191
192
  }
192
193
 
193
- /**
194
- * Loads many objects matching the raw WHERE clause.
195
- *
196
- * @param queryContext - query context in which to perform the load
197
- * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
198
- * @param bindings - array of positional bindings or object of named bindings
199
- * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
200
- * @returns array of objects matching the query
201
- */
202
- async loadManyByRawWhereClauseAsync(
194
+ async countByFieldEqualityConjunctionAsync<N extends keyof TFields>(
203
195
  queryContext: EntityQueryContext,
204
- rawWhereClause: string,
205
- bindings: readonly any[] | object,
206
- querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
207
- ): Promise<readonly Readonly<TFields>[]> {
208
- EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
209
-
210
- return await timeAndLogLoadEventAsync(
196
+ fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
197
+ ): Promise<number> {
198
+ return await timeAndLogCountEventAsync(
211
199
  this.metricsAdapter,
212
- EntityMetricsLoadType.LOAD_MANY_RAW,
200
+ EntityMetricsLoadType.COUNT_EQUALITY_CONJUNCTION,
213
201
  this.entityClassName,
214
202
  queryContext,
215
203
  )(
216
- this.databaseAdapter.fetchManyByRawWhereClauseAsync(
204
+ this.databaseAdapter.countByFieldEqualityConjunctionAsync(
217
205
  queryContext,
218
- rawWhereClause,
219
- bindings,
220
- querySelectionModifiers,
206
+ fieldEqualityOperands,
221
207
  ),
222
208
  );
223
209
  }
@@ -243,6 +229,18 @@ export class EntityKnexDataManager<
243
229
  );
244
230
  }
245
231
 
232
+ async countBySQLFragmentAsync(
233
+ queryContext: EntityQueryContext,
234
+ sqlFragment: SQLFragment<TFields>,
235
+ ): Promise<number> {
236
+ return await timeAndLogCountEventAsync(
237
+ this.metricsAdapter,
238
+ EntityMetricsLoadType.COUNT_SQL,
239
+ this.entityClassName,
240
+ queryContext,
241
+ )(this.databaseAdapter.countBySQLFragmentAsync(queryContext, sqlFragment));
242
+ }
243
+
246
244
  /**
247
245
  * Load a page of objects using cursor-based pagination with unified pagination specification.
248
246
  *
@@ -482,17 +480,16 @@ export class EntityKnexDataManager<
482
480
  baseWhere: SQLFragment<TFields> | undefined,
483
481
  cursorCondition: SQLFragment<TFields> | null,
484
482
  ): SQLFragment<TFields> {
485
- const conditions = [baseWhere, cursorCondition].filter((it) => !!it);
486
- if (conditions.length === 0) {
487
- return sql`TRUE`;
483
+ if (!baseWhere) {
484
+ return cursorCondition ?? sql`TRUE`;
488
485
  }
489
- if (conditions.length === 1) {
490
- return conditions[0]!;
486
+
487
+ if (!cursorCondition) {
488
+ return baseWhere;
491
489
  }
492
- // Wrap baseWhere in parens if combining with cursor condition
493
- // We know we have exactly 2 conditions at this point
494
- const [first, second] = conditions;
495
- return sql`(${first}) AND ${second}`;
490
+
491
+ // Wrap baseWhere in parens when combining with cursor condition
492
+ return sql`(${baseWhere}) AND ${cursorCondition}`;
496
493
  }
497
494
 
498
495
  private augmentOrderByIfNecessary(
@@ -2,17 +2,7 @@ import type { EntityQueryContext, IEntityMetricsAdapter } from '@expo/entity';
2
2
  import { EntityMetricsLoadType, NoOpEntityMetricsAdapter } from '@expo/entity';
3
3
  import { StubQueryContextProvider } from '@expo/entity-testing-utils';
4
4
  import { describe, expect, it } from '@jest/globals';
5
- import {
6
- anyNumber,
7
- anyString,
8
- anything,
9
- deepEqual,
10
- instance,
11
- mock,
12
- resetCalls,
13
- verify,
14
- when,
15
- } from 'ts-mockito';
5
+ import { anyNumber, anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
16
6
 
17
7
  import { OrderByOrdering } from '../../BasePostgresEntityDatabaseAdapter.ts';
18
8
  import { PaginationStrategy } from '../../PaginationStrategy.ts';
@@ -110,14 +100,6 @@ describe(EntityKnexDataManager, () => {
110
100
  nullableField: null,
111
101
  },
112
102
  ]);
113
- when(
114
- databaseAdapterMock.fetchManyByRawWhereClauseAsync(
115
- anything(),
116
- anyString(),
117
- anything(),
118
- anything(),
119
- ),
120
- ).thenResolve([]);
121
103
 
122
104
  const entityDataManager = new EntityKnexDataManager(
123
105
  testEntityConfiguration,
@@ -148,21 +130,6 @@ describe(EntityKnexDataManager, () => {
148
130
  ),
149
131
  ).once();
150
132
 
151
- resetCalls(metricsAdapterMock);
152
-
153
- await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
154
- verify(
155
- metricsAdapterMock.logDataManagerLoadEvent(
156
- deepEqual({
157
- type: EntityMetricsLoadType.LOAD_MANY_RAW,
158
- isInTransaction: false,
159
- entityClassName: TestEntity.name,
160
- duration: anyNumber(),
161
- count: 0,
162
- }),
163
- ),
164
- ).once();
165
-
166
133
  verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
167
134
  });
168
135
 
@@ -190,14 +157,6 @@ describe(EntityKnexDataManager, () => {
190
157
  nullableField: null,
191
158
  },
192
159
  ]);
193
- when(
194
- databaseAdapterMock.fetchManyByRawWhereClauseAsync(
195
- anything(),
196
- anyString(),
197
- anything(),
198
- anything(),
199
- ),
200
- ).thenResolve([]);
201
160
 
202
161
  const entityDataManager = new EntityKnexDataManager(
203
162
  testEntityConfiguration,
@@ -229,21 +188,6 @@ describe(EntityKnexDataManager, () => {
229
188
  ),
230
189
  ).once();
231
190
 
232
- resetCalls(metricsAdapterMock);
233
-
234
- await entityDataManager.loadManyByRawWhereClauseAsync(queryContext, '', [], {});
235
- verify(
236
- metricsAdapterMock.logDataManagerLoadEvent(
237
- deepEqual({
238
- type: EntityMetricsLoadType.LOAD_MANY_RAW,
239
- isInTransaction: true,
240
- entityClassName: TestEntity.name,
241
- duration: anyNumber(),
242
- count: 0,
243
- }),
244
- ),
245
- ).once();
246
-
247
191
  verify(metricsAdapterMock.incrementDataManagerLoadCount(anything())).never();
248
192
  });
249
193
  });