@expo/entity 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/EntityDatabaseAdapter.d.ts +4 -3
- package/build/src/EntityDatabaseAdapter.js +3 -5
- package/build/src/EntityMutationInfo.d.ts +31 -0
- package/build/src/EntityMutationInfo.js +12 -0
- package/build/src/EntityQueryContext.d.ts +52 -3
- package/build/src/EntityQueryContext.js +44 -5
- package/build/src/EntityQueryContextProvider.js +7 -2
- package/build/src/EntitySecondaryCacheLoader.d.ts +1 -2
- package/build/src/EntitySecondaryCacheLoader.js +1 -2
- package/build/src/errors/EntityInvalidFieldValueError.d.ts +2 -0
- package/build/src/errors/EntityInvalidFieldValueError.js +4 -0
- package/build/src/metrics/IEntityMetricsAdapter.d.ts +27 -4
- package/build/src/metrics/IEntityMetricsAdapter.js +27 -4
- package/package.json +2 -2
- package/src/EntityDatabaseAdapter.ts +5 -12
- package/src/EntityMutationInfo.ts +31 -0
- package/src/EntityQueryContext.ts +64 -3
- package/src/EntityQueryContextProvider.ts +9 -3
- package/src/EntitySecondaryCacheLoader.ts +1 -2
- package/src/__tests__/EntityDatabaseAdapter-test.ts +9 -10
- package/src/__tests__/EntityQueryContext-test.ts +84 -0
- package/src/errors/EntityInvalidFieldValueError.ts +5 -0
- package/src/errors/__tests__/EntityError-test.ts +2 -0
- package/src/metrics/IEntityMetricsAdapter.ts +24 -1
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +3 -3
|
@@ -56,10 +56,11 @@ export declare abstract class EntityDatabaseAdapter<TFields extends Record<strin
|
|
|
56
56
|
* @param idField - the field in the object that is the ID
|
|
57
57
|
* @param id - the value of the ID field in the object
|
|
58
58
|
* @param object - the object to update
|
|
59
|
-
* @returns the updated object
|
|
60
59
|
*/
|
|
61
|
-
updateAsync<K extends keyof TFields>(queryContext: EntityQueryContext, idField: K, id: any, object: Readonly<Partial<TFields>>): Promise<
|
|
62
|
-
protected abstract updateInternalAsync(queryInterface: any, tableName: string, tableIdField: string, id: any, object: object): Promise<
|
|
60
|
+
updateAsync<K extends keyof TFields>(queryContext: EntityQueryContext, idField: K, id: any, object: Readonly<Partial<TFields>>): Promise<void>;
|
|
61
|
+
protected abstract updateInternalAsync(queryInterface: any, tableName: string, tableIdField: string, id: any, object: object): Promise<{
|
|
62
|
+
updatedRowCount: number;
|
|
63
|
+
}>;
|
|
63
64
|
/**
|
|
64
65
|
* Delete an object by ID.
|
|
65
66
|
*
|
|
@@ -87,22 +87,20 @@ export class EntityDatabaseAdapter {
|
|
|
87
87
|
* @param idField - the field in the object that is the ID
|
|
88
88
|
* @param id - the value of the ID field in the object
|
|
89
89
|
* @param object - the object to update
|
|
90
|
-
* @returns the updated object
|
|
91
90
|
*/
|
|
92
91
|
async updateAsync(queryContext, idField, id, object) {
|
|
93
92
|
const idColumn = getDatabaseFieldForEntityField(this.entityConfiguration, idField);
|
|
94
93
|
const dbObject = transformFieldsToDatabaseObject(this.entityConfiguration, this.fieldTransformerMap, object);
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
94
|
+
const { updatedRowCount } = await this.updateInternalAsync(queryContext.getQueryInterface(), this.entityConfiguration.tableName, idColumn, id, dbObject);
|
|
95
|
+
if (updatedRowCount > 1) {
|
|
97
96
|
// This should never happen with a properly implemented database adapter unless the underlying table has a non-unique
|
|
98
97
|
// primary key column.
|
|
99
98
|
throw new EntityDatabaseAdapterExcessiveUpdateResultError(`Excessive results from database adapter update: ${this.entityConfiguration.tableName}(id = ${id})`);
|
|
100
99
|
}
|
|
101
|
-
else if (
|
|
100
|
+
else if (updatedRowCount === 0) {
|
|
102
101
|
// This happens when the object to update does not exist. It may have been deleted by another process.
|
|
103
102
|
throw new EntityDatabaseAdapterEmptyUpdateResultError(`Empty results from database adapter update: ${this.entityConfiguration.tableName}(id = ${id})`);
|
|
104
103
|
}
|
|
105
|
-
return transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, results[0]);
|
|
106
104
|
}
|
|
107
105
|
/**
|
|
108
106
|
* Delete an object by ID.
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { Entity } from './Entity.ts';
|
|
2
2
|
import type { ViewerContext } from './ViewerContext.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The type of mutation that is occurring to an entity.
|
|
5
|
+
*/
|
|
3
6
|
export declare enum EntityMutationType {
|
|
7
|
+
/**
|
|
8
|
+
* Create mutation.
|
|
9
|
+
*/
|
|
4
10
|
CREATE = 0,
|
|
11
|
+
/**
|
|
12
|
+
* Update mutation.
|
|
13
|
+
*/
|
|
5
14
|
UPDATE = 1,
|
|
15
|
+
/**
|
|
16
|
+
* Delete mutation.
|
|
17
|
+
*/
|
|
6
18
|
DELETE = 2
|
|
7
19
|
}
|
|
8
20
|
/**
|
|
@@ -19,13 +31,32 @@ export type EntityCascadingDeletionInfo = {
|
|
|
19
31
|
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
20
32
|
};
|
|
21
33
|
type EntityTriggerOrValidatorMutationInfo<TFields extends Record<string, any>, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends Entity<TFields, TIDField, TViewerContext, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> = {
|
|
34
|
+
/**
|
|
35
|
+
* The type of mutation that invoked this trigger or validator.
|
|
36
|
+
*/
|
|
22
37
|
type: EntityMutationType.CREATE;
|
|
23
38
|
} | {
|
|
39
|
+
/**
|
|
40
|
+
* The type of mutation that invoked this trigger or validator.
|
|
41
|
+
*/
|
|
24
42
|
type: EntityMutationType.UPDATE;
|
|
43
|
+
/**
|
|
44
|
+
* The previous value of the entity before the update.
|
|
45
|
+
*/
|
|
25
46
|
previousValue: TEntity;
|
|
47
|
+
/**
|
|
48
|
+
* If this update is part of a cascading deletion (cascade set null), this field will contain information about the cascade
|
|
49
|
+
* that caused this update. Otherwise, it will be null.
|
|
50
|
+
*/
|
|
26
51
|
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
27
52
|
} | {
|
|
53
|
+
/**
|
|
54
|
+
* The type of mutation that invoked this trigger or validator.
|
|
55
|
+
*/
|
|
28
56
|
type: EntityMutationType.DELETE;
|
|
57
|
+
/**
|
|
58
|
+
* If this delete is part of a cascading deletion, this field will contain information about the cascade that caused this cascading delete.
|
|
59
|
+
*/
|
|
29
60
|
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
30
61
|
};
|
|
31
62
|
export type EntityValidatorMutationInfo<TFields extends Record<string, any>, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends Entity<TFields, TIDField, TViewerContext, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> = EntityTriggerOrValidatorMutationInfo<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of mutation that is occurring to an entity.
|
|
3
|
+
*/
|
|
1
4
|
export var EntityMutationType;
|
|
2
5
|
(function (EntityMutationType) {
|
|
6
|
+
/**
|
|
7
|
+
* Create mutation.
|
|
8
|
+
*/
|
|
3
9
|
EntityMutationType[EntityMutationType["CREATE"] = 0] = "CREATE";
|
|
10
|
+
/**
|
|
11
|
+
* Update mutation.
|
|
12
|
+
*/
|
|
4
13
|
EntityMutationType[EntityMutationType["UPDATE"] = 1] = "UPDATE";
|
|
14
|
+
/**
|
|
15
|
+
* Delete mutation.
|
|
16
|
+
*/
|
|
5
17
|
EntityMutationType[EntityMutationType["DELETE"] = 2] = "DELETE";
|
|
6
18
|
})(EntityMutationType || (EntityMutationType = {}));
|
|
@@ -1,20 +1,59 @@
|
|
|
1
1
|
import type { EntityQueryContextProvider } from './EntityQueryContextProvider.ts';
|
|
2
2
|
export type PostCommitCallback = (...args: any) => Promise<any>;
|
|
3
3
|
export type PreCommitCallback = (queryContext: EntityTransactionalQueryContext, ...args: any) => Promise<any>;
|
|
4
|
+
/**
|
|
5
|
+
* Database transaction isolation level. Controls the visibility of changes made by
|
|
6
|
+
* concurrent transactions, trading off between consistency and performance.
|
|
7
|
+
*/
|
|
4
8
|
export declare enum TransactionIsolationLevel {
|
|
9
|
+
/**
|
|
10
|
+
* Each statement sees only data committed before it began. Default for most databases.
|
|
11
|
+
*/
|
|
5
12
|
READ_COMMITTED = "READ_COMMITTED",
|
|
13
|
+
/**
|
|
14
|
+
* All statements in the transaction see the same snapshot taken at the start of the transaction.
|
|
15
|
+
*/
|
|
6
16
|
REPEATABLE_READ = "REPEATABLE_READ",
|
|
17
|
+
/**
|
|
18
|
+
* Transactions execute as if they were run one at a time. Strongest guarantee but lowest throughput.
|
|
19
|
+
*/
|
|
7
20
|
SERIALIZABLE = "SERIALIZABLE"
|
|
8
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Controls DataLoader behavior within a transaction.
|
|
24
|
+
*/
|
|
9
25
|
export declare enum TransactionalDataLoaderMode {
|
|
26
|
+
/**
|
|
27
|
+
* Default mode where DataLoader is fully enabled, providing both batching and caching benefits within the transaction.
|
|
28
|
+
*/
|
|
10
29
|
ENABLED = "ENABLED",
|
|
30
|
+
/**
|
|
31
|
+
* DataLoader is enabled for batching queries together but does not cache results. Use this mode when you want to benefit from batching but need to ensure that each load reflects the most current database state, even within the same transaction.
|
|
32
|
+
*/
|
|
11
33
|
ENABLED_BATCH_ONLY = "ENABLED_BATCH_ONLY",
|
|
34
|
+
/**
|
|
35
|
+
* DataLoader is completely disabled for the transaction. Each load will directly query the database without any batching or caching. Use this mode when you need to ensure that every load reflects the most current database state and are not concerned about the performance benefits of batching or caching within the transaction.
|
|
36
|
+
*/
|
|
12
37
|
DISABLED = "DISABLED"
|
|
13
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Configuration options for running a transaction. This includes the isolation level and DataLoader mode for the transaction.
|
|
41
|
+
*/
|
|
14
42
|
export type TransactionConfig = {
|
|
43
|
+
/**
|
|
44
|
+
* Transaction isolation level. When omitted, the database default is used (typically READ_COMMITTED).
|
|
45
|
+
*/
|
|
15
46
|
isolationLevel?: TransactionIsolationLevel;
|
|
47
|
+
/**
|
|
48
|
+
* DataLoader mode for the transaction. When omitted, defaults to ENABLED.
|
|
49
|
+
*/
|
|
16
50
|
transactionalDataLoaderMode?: TransactionalDataLoaderMode;
|
|
17
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolved transaction configuration with all default values filled in.
|
|
54
|
+
* This is the configuration that is actually used for a transaction after applying defaults.
|
|
55
|
+
*/
|
|
56
|
+
export type ResolvedTransactionConfig = Pick<TransactionConfig, 'isolationLevel'> & Required<Pick<TransactionConfig, 'transactionalDataLoaderMode'>>;
|
|
18
57
|
/**
|
|
19
58
|
* Entity framework representation of transactional and non-transactional database
|
|
20
59
|
* query execution units.
|
|
@@ -52,7 +91,7 @@ export declare class EntityTransactionalQueryContext extends EntityQueryContext
|
|
|
52
91
|
* @internal
|
|
53
92
|
*/
|
|
54
93
|
readonly transactionId: string;
|
|
55
|
-
readonly
|
|
94
|
+
readonly transactionConfig: ResolvedTransactionConfig;
|
|
56
95
|
/**
|
|
57
96
|
* @internal
|
|
58
97
|
*/
|
|
@@ -64,7 +103,17 @@ export declare class EntityTransactionalQueryContext extends EntityQueryContext
|
|
|
64
103
|
/**
|
|
65
104
|
* @internal
|
|
66
105
|
*/
|
|
67
|
-
transactionId: string,
|
|
106
|
+
transactionId: string, transactionConfig: ResolvedTransactionConfig);
|
|
107
|
+
/**
|
|
108
|
+
* DataLoader mode for this transaction set at time of transaction creation, controlling DataLoader caching and batching behavior within the transaction.
|
|
109
|
+
*/
|
|
110
|
+
get transactionalDataLoaderMode(): TransactionalDataLoaderMode;
|
|
111
|
+
/**
|
|
112
|
+
* Transaction isolation level for this transaction set at time of transaction creation.
|
|
113
|
+
* This controls the visibility of changes made by concurrent transactions.
|
|
114
|
+
* When undefined, the database default isolation level is used (typically READ_COMMITTED).
|
|
115
|
+
*/
|
|
116
|
+
get isolationLevel(): TransactionIsolationLevel | undefined;
|
|
68
117
|
/**
|
|
69
118
|
* Schedule a pre-commit callback. These will be run within the transaction right before it is
|
|
70
119
|
* committed, and will be run in the order specified. Ordering of callbacks scheduled with the
|
|
@@ -118,7 +167,7 @@ export declare class EntityNestedTransactionalQueryContext extends EntityTransac
|
|
|
118
167
|
/**
|
|
119
168
|
* @internal
|
|
120
169
|
*/
|
|
121
|
-
parentQueryContext: EntityTransactionalQueryContext, entityQueryContextProvider: EntityQueryContextProvider, transactionId: string,
|
|
170
|
+
parentQueryContext: EntityTransactionalQueryContext, entityQueryContextProvider: EntityQueryContextProvider, transactionId: string, transactionConfig: ResolvedTransactionConfig);
|
|
122
171
|
isInNestedTransaction(): this is EntityNestedTransactionalQueryContext;
|
|
123
172
|
appendPostCommitCallback(callback: PostCommitCallback): void;
|
|
124
173
|
appendPostCommitInvalidationCallback(callback: PostCommitCallback): void;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import assert from 'assert';
|
|
2
|
+
/**
|
|
3
|
+
* Database transaction isolation level. Controls the visibility of changes made by
|
|
4
|
+
* concurrent transactions, trading off between consistency and performance.
|
|
5
|
+
*/
|
|
2
6
|
export var TransactionIsolationLevel;
|
|
3
7
|
(function (TransactionIsolationLevel) {
|
|
8
|
+
/**
|
|
9
|
+
* Each statement sees only data committed before it began. Default for most databases.
|
|
10
|
+
*/
|
|
4
11
|
TransactionIsolationLevel["READ_COMMITTED"] = "READ_COMMITTED";
|
|
12
|
+
/**
|
|
13
|
+
* All statements in the transaction see the same snapshot taken at the start of the transaction.
|
|
14
|
+
*/
|
|
5
15
|
TransactionIsolationLevel["REPEATABLE_READ"] = "REPEATABLE_READ";
|
|
16
|
+
/**
|
|
17
|
+
* Transactions execute as if they were run one at a time. Strongest guarantee but lowest throughput.
|
|
18
|
+
*/
|
|
6
19
|
TransactionIsolationLevel["SERIALIZABLE"] = "SERIALIZABLE";
|
|
7
20
|
})(TransactionIsolationLevel || (TransactionIsolationLevel = {}));
|
|
21
|
+
/**
|
|
22
|
+
* Controls DataLoader behavior within a transaction.
|
|
23
|
+
*/
|
|
8
24
|
export var TransactionalDataLoaderMode;
|
|
9
25
|
(function (TransactionalDataLoaderMode) {
|
|
26
|
+
/**
|
|
27
|
+
* Default mode where DataLoader is fully enabled, providing both batching and caching benefits within the transaction.
|
|
28
|
+
*/
|
|
10
29
|
TransactionalDataLoaderMode["ENABLED"] = "ENABLED";
|
|
30
|
+
/**
|
|
31
|
+
* DataLoader is enabled for batching queries together but does not cache results. Use this mode when you want to benefit from batching but need to ensure that each load reflects the most current database state, even within the same transaction.
|
|
32
|
+
*/
|
|
11
33
|
TransactionalDataLoaderMode["ENABLED_BATCH_ONLY"] = "ENABLED_BATCH_ONLY";
|
|
34
|
+
/**
|
|
35
|
+
* DataLoader is completely disabled for the transaction. Each load will directly query the database without any batching or caching. Use this mode when you need to ensure that every load reflects the most current database state and are not concerned about the performance benefits of batching or caching within the transaction.
|
|
36
|
+
*/
|
|
12
37
|
TransactionalDataLoaderMode["DISABLED"] = "DISABLED";
|
|
13
38
|
})(TransactionalDataLoaderMode || (TransactionalDataLoaderMode = {}));
|
|
14
39
|
/**
|
|
@@ -54,7 +79,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|
|
54
79
|
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
55
80
|
entityQueryContextProvider;
|
|
56
81
|
transactionId;
|
|
57
|
-
|
|
82
|
+
transactionConfig;
|
|
58
83
|
/**
|
|
59
84
|
* @internal
|
|
60
85
|
*/
|
|
@@ -66,11 +91,25 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
66
91
|
/**
|
|
67
92
|
* @internal
|
|
68
93
|
*/
|
|
69
|
-
transactionId,
|
|
94
|
+
transactionId, transactionConfig) {
|
|
70
95
|
super(queryInterface);
|
|
71
96
|
this.entityQueryContextProvider = entityQueryContextProvider;
|
|
72
97
|
this.transactionId = transactionId;
|
|
73
|
-
this.
|
|
98
|
+
this.transactionConfig = transactionConfig;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* DataLoader mode for this transaction set at time of transaction creation, controlling DataLoader caching and batching behavior within the transaction.
|
|
102
|
+
*/
|
|
103
|
+
get transactionalDataLoaderMode() {
|
|
104
|
+
return this.transactionConfig.transactionalDataLoaderMode;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Transaction isolation level for this transaction set at time of transaction creation.
|
|
108
|
+
* This controls the visibility of changes made by concurrent transactions.
|
|
109
|
+
* When undefined, the database default isolation level is used (typically READ_COMMITTED).
|
|
110
|
+
*/
|
|
111
|
+
get isolationLevel() {
|
|
112
|
+
return this.transactionConfig.isolationLevel;
|
|
74
113
|
}
|
|
75
114
|
/**
|
|
76
115
|
* Schedule a pre-commit callback. These will be run within the transaction right before it is
|
|
@@ -153,8 +192,8 @@ export class EntityNestedTransactionalQueryContext extends EntityTransactionalQu
|
|
|
153
192
|
/**
|
|
154
193
|
* @internal
|
|
155
194
|
*/
|
|
156
|
-
parentQueryContext, entityQueryContextProvider, transactionId,
|
|
157
|
-
super(queryInterface, entityQueryContextProvider, transactionId,
|
|
195
|
+
parentQueryContext, entityQueryContextProvider, transactionId, transactionConfig) {
|
|
196
|
+
super(queryInterface, entityQueryContextProvider, transactionId, transactionConfig);
|
|
158
197
|
this.parentQueryContext = parentQueryContext;
|
|
159
198
|
parentQueryContext.childQueryContexts.push(this);
|
|
160
199
|
}
|
|
@@ -22,7 +22,12 @@ export class EntityQueryContextProvider {
|
|
|
22
22
|
*/
|
|
23
23
|
async runInTransactionAsync(transactionScope, transactionConfig) {
|
|
24
24
|
const [returnedValue, queryContext] = await this.createTransactionRunner(transactionConfig)(async (queryInterface) => {
|
|
25
|
-
const
|
|
25
|
+
const resolvedTransactionConfig = {
|
|
26
|
+
...transactionConfig,
|
|
27
|
+
transactionalDataLoaderMode: transactionConfig?.transactionalDataLoaderMode ??
|
|
28
|
+
this.defaultTransactionalDataLoaderMode(),
|
|
29
|
+
};
|
|
30
|
+
const queryContext = new EntityTransactionalQueryContext(queryInterface, this, randomUUID(), resolvedTransactionConfig);
|
|
26
31
|
const result = await transactionScope(queryContext);
|
|
27
32
|
await queryContext.runPreCommitCallbacksAsync();
|
|
28
33
|
return [result, queryContext];
|
|
@@ -38,7 +43,7 @@ export class EntityQueryContextProvider {
|
|
|
38
43
|
*/
|
|
39
44
|
async runInNestedTransactionAsync(outerQueryContext, transactionScope) {
|
|
40
45
|
const [returnedValue, innerQueryContext] = await this.createNestedTransactionRunner(outerQueryContext.getQueryInterface())(async (innerQueryInterface) => {
|
|
41
|
-
const innerQueryContext = new EntityNestedTransactionalQueryContext(innerQueryInterface, outerQueryContext, this, randomUUID(), outerQueryContext.
|
|
46
|
+
const innerQueryContext = new EntityNestedTransactionalQueryContext(innerQueryInterface, outerQueryContext, this, randomUUID(), outerQueryContext.transactionConfig);
|
|
42
47
|
const result = await transactionScope(innerQueryContext);
|
|
43
48
|
await innerQueryContext.runPreCommitCallbacksAsync();
|
|
44
49
|
return [result, innerQueryContext];
|
|
@@ -32,8 +32,7 @@ export interface ISecondaryEntityCache<TFields extends Record<string, any>, TLoa
|
|
|
32
32
|
* when the underlying data of a cache key could be stale.
|
|
33
33
|
*
|
|
34
34
|
* This is most commonly used to further optimize hot paths that cannot make use of normal entity cache loading
|
|
35
|
-
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync
|
|
36
|
-
* `loadManyByRawWhereClauseAsync`.
|
|
35
|
+
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync`.
|
|
37
36
|
*/
|
|
38
37
|
export declare abstract class EntitySecondaryCacheLoader<TLoadParams, TFields extends Record<string, any>, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> {
|
|
39
38
|
private readonly secondaryEntityCache;
|
|
@@ -7,8 +7,7 @@ import { mapMap } from "./utils/collections/maps.js";
|
|
|
7
7
|
* when the underlying data of a cache key could be stale.
|
|
8
8
|
*
|
|
9
9
|
* This is most commonly used to further optimize hot paths that cannot make use of normal entity cache loading
|
|
10
|
-
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync
|
|
11
|
-
* `loadManyByRawWhereClauseAsync`.
|
|
10
|
+
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync`.
|
|
12
11
|
*/
|
|
13
12
|
export class EntitySecondaryCacheLoader {
|
|
14
13
|
secondaryEntityCache;
|
|
@@ -9,5 +9,7 @@ import { EntityError, EntityErrorCode, EntityErrorState } from './EntityError.ts
|
|
|
9
9
|
export declare class EntityInvalidFieldValueError<TFields extends Record<string, any>, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, N extends keyof TFields, TSelectedFields extends keyof TFields = keyof TFields> extends EntityError {
|
|
10
10
|
get state(): EntityErrorState.PERMANENT;
|
|
11
11
|
get code(): EntityErrorCode.ERR_ENTITY_INVALID_FIELD_VALUE;
|
|
12
|
+
readonly fieldName: N;
|
|
13
|
+
readonly fieldValue: TFields[N] | undefined;
|
|
12
14
|
constructor(entityClass: IEntityClass<TFields, TIDField, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>, fieldName: N, fieldValue?: TFields[N]);
|
|
13
15
|
}
|
|
@@ -12,7 +12,11 @@ export class EntityInvalidFieldValueError extends EntityError {
|
|
|
12
12
|
get code() {
|
|
13
13
|
return EntityErrorCode.ERR_ENTITY_INVALID_FIELD_VALUE;
|
|
14
14
|
}
|
|
15
|
+
fieldName;
|
|
16
|
+
fieldValue;
|
|
15
17
|
constructor(entityClass, fieldName, fieldValue) {
|
|
16
18
|
super(`Entity field not valid: ${entityClass.name} (${String(fieldName)} = ${fieldValue})`);
|
|
19
|
+
this.fieldName = fieldName;
|
|
20
|
+
this.fieldValue = fieldValue;
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import type { EntityAuthorizationAction, EntityPrivacyPolicyEvaluationMode } from '../EntityPrivacyPolicy.ts';
|
|
2
2
|
import type { EntityLoadMethodType } from '../internal/EntityLoadInterfaces.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The type of the load method being called.
|
|
5
|
+
*/
|
|
3
6
|
export declare enum EntityMetricsLoadType {
|
|
7
|
+
/**
|
|
8
|
+
* Standard EntityLoader load (the methods from EnforcingEntityLoader or AuthorizationResultBasedEntityLoader).
|
|
9
|
+
*/
|
|
4
10
|
LOAD_MANY = 0,
|
|
11
|
+
/**
|
|
12
|
+
* Knex loader load using loadManyByFieldEqualityConjunctionAsync.
|
|
13
|
+
*/
|
|
5
14
|
LOAD_MANY_EQUALITY_CONJUNCTION = 1,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Knex loader load using loadManyBySQL.
|
|
17
|
+
*/
|
|
18
|
+
LOAD_MANY_SQL = 2,
|
|
19
|
+
/**
|
|
20
|
+
* Internal data manager load via database adapter method loadOneEqualingAsync.
|
|
21
|
+
*/
|
|
22
|
+
LOAD_ONE = 3,
|
|
23
|
+
/**
|
|
24
|
+
* Knex loader load using loadPageAsync.
|
|
25
|
+
*/
|
|
26
|
+
LOAD_PAGE = 4
|
|
10
27
|
}
|
|
11
28
|
/**
|
|
12
29
|
* Event about a single call to an EntityLoader method.
|
|
@@ -33,6 +50,9 @@ export interface EntityMetricsLoadEvent {
|
|
|
33
50
|
*/
|
|
34
51
|
count: number;
|
|
35
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The type of mutation being performed.
|
|
55
|
+
*/
|
|
36
56
|
export declare enum EntityMetricsMutationType {
|
|
37
57
|
CREATE = 0,
|
|
38
58
|
UPDATE = 1,
|
|
@@ -56,6 +76,9 @@ export interface EntityMetricsMutationEvent {
|
|
|
56
76
|
*/
|
|
57
77
|
duration: number;
|
|
58
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Type used to delineate dataloader, cache, and database load counts in EntityDataManager.
|
|
81
|
+
*/
|
|
59
82
|
export declare enum IncrementLoadCountEventType {
|
|
60
83
|
/**
|
|
61
84
|
* Type for when a dataloader load is initiated via the standard load methods
|
|
@@ -1,18 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of the load method being called.
|
|
3
|
+
*/
|
|
1
4
|
export var EntityMetricsLoadType;
|
|
2
5
|
(function (EntityMetricsLoadType) {
|
|
6
|
+
/**
|
|
7
|
+
* Standard EntityLoader load (the methods from EnforcingEntityLoader or AuthorizationResultBasedEntityLoader).
|
|
8
|
+
*/
|
|
3
9
|
EntityMetricsLoadType[EntityMetricsLoadType["LOAD_MANY"] = 0] = "LOAD_MANY";
|
|
10
|
+
/**
|
|
11
|
+
* Knex loader load using loadManyByFieldEqualityConjunctionAsync.
|
|
12
|
+
*/
|
|
4
13
|
EntityMetricsLoadType[EntityMetricsLoadType["LOAD_MANY_EQUALITY_CONJUNCTION"] = 1] = "LOAD_MANY_EQUALITY_CONJUNCTION";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
EntityMetricsLoadType[EntityMetricsLoadType["
|
|
14
|
+
/**
|
|
15
|
+
* Knex loader load using loadManyBySQL.
|
|
16
|
+
*/
|
|
17
|
+
EntityMetricsLoadType[EntityMetricsLoadType["LOAD_MANY_SQL"] = 2] = "LOAD_MANY_SQL";
|
|
18
|
+
/**
|
|
19
|
+
* Internal data manager load via database adapter method loadOneEqualingAsync.
|
|
20
|
+
*/
|
|
21
|
+
EntityMetricsLoadType[EntityMetricsLoadType["LOAD_ONE"] = 3] = "LOAD_ONE";
|
|
22
|
+
/**
|
|
23
|
+
* Knex loader load using loadPageAsync.
|
|
24
|
+
*/
|
|
25
|
+
EntityMetricsLoadType[EntityMetricsLoadType["LOAD_PAGE"] = 4] = "LOAD_PAGE";
|
|
9
26
|
})(EntityMetricsLoadType || (EntityMetricsLoadType = {}));
|
|
27
|
+
/**
|
|
28
|
+
* The type of mutation being performed.
|
|
29
|
+
*/
|
|
10
30
|
export var EntityMetricsMutationType;
|
|
11
31
|
(function (EntityMetricsMutationType) {
|
|
12
32
|
EntityMetricsMutationType[EntityMetricsMutationType["CREATE"] = 0] = "CREATE";
|
|
13
33
|
EntityMetricsMutationType[EntityMetricsMutationType["UPDATE"] = 1] = "UPDATE";
|
|
14
34
|
EntityMetricsMutationType[EntityMetricsMutationType["DELETE"] = 2] = "DELETE";
|
|
15
35
|
})(EntityMetricsMutationType || (EntityMetricsMutationType = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Type used to delineate dataloader, cache, and database load counts in EntityDataManager.
|
|
38
|
+
*/
|
|
16
39
|
export var IncrementLoadCountEventType;
|
|
17
40
|
(function (IncrementLoadCountEventType) {
|
|
18
41
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "A privacy-first data model",
|
|
5
5
|
"files": [
|
|
6
6
|
"build",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"typescript": "5.9.3",
|
|
43
43
|
"uuid": "13.0.0"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "dbd1cc847952754acc0eb407165cbb7b8350d2df"
|
|
46
46
|
}
|
|
@@ -202,21 +202,20 @@ export abstract class EntityDatabaseAdapter<
|
|
|
202
202
|
* @param idField - the field in the object that is the ID
|
|
203
203
|
* @param id - the value of the ID field in the object
|
|
204
204
|
* @param object - the object to update
|
|
205
|
-
* @returns the updated object
|
|
206
205
|
*/
|
|
207
206
|
async updateAsync<K extends keyof TFields>(
|
|
208
207
|
queryContext: EntityQueryContext,
|
|
209
208
|
idField: K,
|
|
210
209
|
id: any,
|
|
211
210
|
object: Readonly<Partial<TFields>>,
|
|
212
|
-
): Promise<
|
|
211
|
+
): Promise<void> {
|
|
213
212
|
const idColumn = getDatabaseFieldForEntityField(this.entityConfiguration, idField);
|
|
214
213
|
const dbObject = transformFieldsToDatabaseObject(
|
|
215
214
|
this.entityConfiguration,
|
|
216
215
|
this.fieldTransformerMap,
|
|
217
216
|
object,
|
|
218
217
|
);
|
|
219
|
-
const
|
|
218
|
+
const { updatedRowCount } = await this.updateInternalAsync(
|
|
220
219
|
queryContext.getQueryInterface(),
|
|
221
220
|
this.entityConfiguration.tableName,
|
|
222
221
|
idColumn,
|
|
@@ -224,24 +223,18 @@ export abstract class EntityDatabaseAdapter<
|
|
|
224
223
|
dbObject,
|
|
225
224
|
);
|
|
226
225
|
|
|
227
|
-
if (
|
|
226
|
+
if (updatedRowCount > 1) {
|
|
228
227
|
// This should never happen with a properly implemented database adapter unless the underlying table has a non-unique
|
|
229
228
|
// primary key column.
|
|
230
229
|
throw new EntityDatabaseAdapterExcessiveUpdateResultError(
|
|
231
230
|
`Excessive results from database adapter update: ${this.entityConfiguration.tableName}(id = ${id})`,
|
|
232
231
|
);
|
|
233
|
-
} else if (
|
|
232
|
+
} else if (updatedRowCount === 0) {
|
|
234
233
|
// This happens when the object to update does not exist. It may have been deleted by another process.
|
|
235
234
|
throw new EntityDatabaseAdapterEmptyUpdateResultError(
|
|
236
235
|
`Empty results from database adapter update: ${this.entityConfiguration.tableName}(id = ${id})`,
|
|
237
236
|
);
|
|
238
237
|
}
|
|
239
|
-
|
|
240
|
-
return transformDatabaseObjectToFields(
|
|
241
|
-
this.entityConfiguration,
|
|
242
|
-
this.fieldTransformerMap,
|
|
243
|
-
results[0]!,
|
|
244
|
-
);
|
|
245
238
|
}
|
|
246
239
|
|
|
247
240
|
protected abstract updateInternalAsync(
|
|
@@ -250,7 +243,7 @@ export abstract class EntityDatabaseAdapter<
|
|
|
250
243
|
tableIdField: string,
|
|
251
244
|
id: any,
|
|
252
245
|
object: object,
|
|
253
|
-
): Promise<
|
|
246
|
+
): Promise<{ updatedRowCount: number }>;
|
|
254
247
|
|
|
255
248
|
/**
|
|
256
249
|
* Delete an object by ID.
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { Entity } from './Entity.ts';
|
|
2
2
|
import type { ViewerContext } from './ViewerContext.ts';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The type of mutation that is occurring to an entity.
|
|
6
|
+
*/
|
|
4
7
|
export enum EntityMutationType {
|
|
8
|
+
/**
|
|
9
|
+
* Create mutation.
|
|
10
|
+
*/
|
|
5
11
|
CREATE,
|
|
12
|
+
/**
|
|
13
|
+
* Update mutation.
|
|
14
|
+
*/
|
|
6
15
|
UPDATE,
|
|
16
|
+
/**
|
|
17
|
+
* Delete mutation.
|
|
18
|
+
*/
|
|
7
19
|
DELETE,
|
|
8
20
|
}
|
|
9
21
|
|
|
@@ -30,15 +42,34 @@ type EntityTriggerOrValidatorMutationInfo<
|
|
|
30
42
|
TSelectedFields extends keyof TFields = keyof TFields,
|
|
31
43
|
> =
|
|
32
44
|
| {
|
|
45
|
+
/**
|
|
46
|
+
* The type of mutation that invoked this trigger or validator.
|
|
47
|
+
*/
|
|
33
48
|
type: EntityMutationType.CREATE;
|
|
34
49
|
}
|
|
35
50
|
| {
|
|
51
|
+
/**
|
|
52
|
+
* The type of mutation that invoked this trigger or validator.
|
|
53
|
+
*/
|
|
36
54
|
type: EntityMutationType.UPDATE;
|
|
55
|
+
/**
|
|
56
|
+
* The previous value of the entity before the update.
|
|
57
|
+
*/
|
|
37
58
|
previousValue: TEntity;
|
|
59
|
+
/**
|
|
60
|
+
* If this update is part of a cascading deletion (cascade set null), this field will contain information about the cascade
|
|
61
|
+
* that caused this update. Otherwise, it will be null.
|
|
62
|
+
*/
|
|
38
63
|
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
39
64
|
}
|
|
40
65
|
| {
|
|
66
|
+
/**
|
|
67
|
+
* The type of mutation that invoked this trigger or validator.
|
|
68
|
+
*/
|
|
41
69
|
type: EntityMutationType.DELETE;
|
|
70
|
+
/**
|
|
71
|
+
* If this delete is part of a cascading deletion, this field will contain information about the cascade that caused this cascading delete.
|
|
72
|
+
*/
|
|
42
73
|
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
43
74
|
};
|
|
44
75
|
|
|
@@ -8,23 +8,68 @@ export type PreCommitCallback = (
|
|
|
8
8
|
...args: any
|
|
9
9
|
) => Promise<any>;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Database transaction isolation level. Controls the visibility of changes made by
|
|
13
|
+
* concurrent transactions, trading off between consistency and performance.
|
|
14
|
+
*/
|
|
11
15
|
export enum TransactionIsolationLevel {
|
|
16
|
+
/**
|
|
17
|
+
* Each statement sees only data committed before it began. Default for most databases.
|
|
18
|
+
*/
|
|
12
19
|
READ_COMMITTED = 'READ_COMMITTED',
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* All statements in the transaction see the same snapshot taken at the start of the transaction.
|
|
23
|
+
*/
|
|
13
24
|
REPEATABLE_READ = 'REPEATABLE_READ',
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Transactions execute as if they were run one at a time. Strongest guarantee but lowest throughput.
|
|
28
|
+
*/
|
|
14
29
|
SERIALIZABLE = 'SERIALIZABLE',
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Controls DataLoader behavior within a transaction.
|
|
34
|
+
*/
|
|
17
35
|
export enum TransactionalDataLoaderMode {
|
|
36
|
+
/**
|
|
37
|
+
* Default mode where DataLoader is fully enabled, providing both batching and caching benefits within the transaction.
|
|
38
|
+
*/
|
|
18
39
|
ENABLED = 'ENABLED',
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DataLoader is enabled for batching queries together but does not cache results. Use this mode when you want to benefit from batching but need to ensure that each load reflects the most current database state, even within the same transaction.
|
|
43
|
+
*/
|
|
19
44
|
ENABLED_BATCH_ONLY = 'ENABLED_BATCH_ONLY',
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* DataLoader is completely disabled for the transaction. Each load will directly query the database without any batching or caching. Use this mode when you need to ensure that every load reflects the most current database state and are not concerned about the performance benefits of batching or caching within the transaction.
|
|
48
|
+
*/
|
|
20
49
|
DISABLED = 'DISABLED',
|
|
21
50
|
}
|
|
22
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Configuration options for running a transaction. This includes the isolation level and DataLoader mode for the transaction.
|
|
54
|
+
*/
|
|
23
55
|
export type TransactionConfig = {
|
|
56
|
+
/**
|
|
57
|
+
* Transaction isolation level. When omitted, the database default is used (typically READ_COMMITTED).
|
|
58
|
+
*/
|
|
24
59
|
isolationLevel?: TransactionIsolationLevel;
|
|
60
|
+
/**
|
|
61
|
+
* DataLoader mode for the transaction. When omitted, defaults to ENABLED.
|
|
62
|
+
*/
|
|
25
63
|
transactionalDataLoaderMode?: TransactionalDataLoaderMode;
|
|
26
64
|
};
|
|
27
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Resolved transaction configuration with all default values filled in.
|
|
68
|
+
* This is the configuration that is actually used for a transaction after applying defaults.
|
|
69
|
+
*/
|
|
70
|
+
export type ResolvedTransactionConfig = Pick<TransactionConfig, 'isolationLevel'> &
|
|
71
|
+
Required<Pick<TransactionConfig, 'transactionalDataLoaderMode'>>;
|
|
72
|
+
|
|
28
73
|
/**
|
|
29
74
|
* Entity framework representation of transactional and non-transactional database
|
|
30
75
|
* query execution units.
|
|
@@ -99,11 +144,27 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
99
144
|
* @internal
|
|
100
145
|
*/
|
|
101
146
|
readonly transactionId: string,
|
|
102
|
-
public readonly
|
|
147
|
+
public readonly transactionConfig: ResolvedTransactionConfig,
|
|
103
148
|
) {
|
|
104
149
|
super(queryInterface);
|
|
105
150
|
}
|
|
106
151
|
|
|
152
|
+
/**
|
|
153
|
+
* DataLoader mode for this transaction set at time of transaction creation, controlling DataLoader caching and batching behavior within the transaction.
|
|
154
|
+
*/
|
|
155
|
+
get transactionalDataLoaderMode(): TransactionalDataLoaderMode {
|
|
156
|
+
return this.transactionConfig.transactionalDataLoaderMode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Transaction isolation level for this transaction set at time of transaction creation.
|
|
161
|
+
* This controls the visibility of changes made by concurrent transactions.
|
|
162
|
+
* When undefined, the database default isolation level is used (typically READ_COMMITTED).
|
|
163
|
+
*/
|
|
164
|
+
get isolationLevel(): TransactionIsolationLevel | undefined {
|
|
165
|
+
return this.transactionConfig.isolationLevel;
|
|
166
|
+
}
|
|
167
|
+
|
|
107
168
|
/**
|
|
108
169
|
* Schedule a pre-commit callback. These will be run within the transaction right before it is
|
|
109
170
|
* committed, and will be run in the order specified. Ordering of callbacks scheduled with the
|
|
@@ -214,9 +275,9 @@ export class EntityNestedTransactionalQueryContext extends EntityTransactionalQu
|
|
|
214
275
|
readonly parentQueryContext: EntityTransactionalQueryContext,
|
|
215
276
|
entityQueryContextProvider: EntityQueryContextProvider,
|
|
216
277
|
transactionId: string,
|
|
217
|
-
|
|
278
|
+
transactionConfig: ResolvedTransactionConfig,
|
|
218
279
|
) {
|
|
219
|
-
super(queryInterface, entityQueryContextProvider, transactionId,
|
|
280
|
+
super(queryInterface, entityQueryContextProvider, transactionId, transactionConfig);
|
|
220
281
|
parentQueryContext.childQueryContexts.push(this);
|
|
221
282
|
}
|
|
222
283
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import type { TransactionConfig } from './EntityQueryContext.ts';
|
|
3
|
+
import type { ResolvedTransactionConfig, TransactionConfig } from './EntityQueryContext.ts';
|
|
4
4
|
import {
|
|
5
5
|
EntityNestedTransactionalQueryContext,
|
|
6
6
|
EntityNonTransactionalQueryContext,
|
|
@@ -53,11 +53,17 @@ export abstract class EntityQueryContextProvider {
|
|
|
53
53
|
const [returnedValue, queryContext] = await this.createTransactionRunner<
|
|
54
54
|
[T, EntityTransactionalQueryContext]
|
|
55
55
|
>(transactionConfig)(async (queryInterface) => {
|
|
56
|
+
const resolvedTransactionConfig: ResolvedTransactionConfig = {
|
|
57
|
+
...transactionConfig,
|
|
58
|
+
transactionalDataLoaderMode:
|
|
59
|
+
transactionConfig?.transactionalDataLoaderMode ??
|
|
60
|
+
this.defaultTransactionalDataLoaderMode(),
|
|
61
|
+
};
|
|
56
62
|
const queryContext = new EntityTransactionalQueryContext(
|
|
57
63
|
queryInterface,
|
|
58
64
|
this,
|
|
59
65
|
randomUUID(),
|
|
60
|
-
|
|
66
|
+
resolvedTransactionConfig,
|
|
61
67
|
);
|
|
62
68
|
const result = await transactionScope(queryContext);
|
|
63
69
|
await queryContext.runPreCommitCallbacksAsync();
|
|
@@ -85,7 +91,7 @@ export abstract class EntityQueryContextProvider {
|
|
|
85
91
|
outerQueryContext,
|
|
86
92
|
this,
|
|
87
93
|
randomUUID(),
|
|
88
|
-
outerQueryContext.
|
|
94
|
+
outerQueryContext.transactionConfig,
|
|
89
95
|
);
|
|
90
96
|
const result = await transactionScope(innerQueryContext);
|
|
91
97
|
await innerQueryContext.runPreCommitCallbacksAsync();
|
|
@@ -42,8 +42,7 @@ export interface ISecondaryEntityCache<TFields extends Record<string, any>, TLoa
|
|
|
42
42
|
* when the underlying data of a cache key could be stale.
|
|
43
43
|
*
|
|
44
44
|
* This is most commonly used to further optimize hot paths that cannot make use of normal entity cache loading
|
|
45
|
-
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync
|
|
46
|
-
* `loadManyByRawWhereClauseAsync`.
|
|
45
|
+
* due to use of a non-unique-field-based EntityLoader method like `loadManyByFieldEqualityConjunctionAsync`.
|
|
47
46
|
*/
|
|
48
47
|
export abstract class EntitySecondaryCacheLoader<
|
|
49
48
|
TLoadParams,
|
|
@@ -23,20 +23,20 @@ class TestEntityDatabaseAdapter extends EntityDatabaseAdapter<TestFields, 'custo
|
|
|
23
23
|
private readonly fetchResults: object[];
|
|
24
24
|
private readonly fetchOneResult: object | null;
|
|
25
25
|
private readonly insertResults: object[];
|
|
26
|
-
private readonly updateResults:
|
|
26
|
+
private readonly updateResults: { updatedRowCount: number };
|
|
27
27
|
private readonly deleteCount: number;
|
|
28
28
|
|
|
29
29
|
constructor({
|
|
30
30
|
fetchResults = [],
|
|
31
31
|
fetchOneResult = null,
|
|
32
32
|
insertResults = [],
|
|
33
|
-
updateResults =
|
|
33
|
+
updateResults = { updatedRowCount: 0 },
|
|
34
34
|
deleteCount = 0,
|
|
35
35
|
}: {
|
|
36
36
|
fetchResults?: object[];
|
|
37
37
|
fetchOneResult?: object | null;
|
|
38
38
|
insertResults?: object[];
|
|
39
|
-
updateResults?:
|
|
39
|
+
updateResults?: { updatedRowCount: number };
|
|
40
40
|
deleteCount?: number;
|
|
41
41
|
}) {
|
|
42
42
|
super(testEntityConfiguration);
|
|
@@ -83,7 +83,7 @@ class TestEntityDatabaseAdapter extends EntityDatabaseAdapter<TestFields, 'custo
|
|
|
83
83
|
_tableIdField: string,
|
|
84
84
|
_id: any,
|
|
85
85
|
_object: object,
|
|
86
|
-
): Promise<
|
|
86
|
+
): Promise<{ updatedRowCount: number }> {
|
|
87
87
|
return this.updateResults;
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -256,16 +256,15 @@ describe(EntityDatabaseAdapter, () => {
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
describe('updateAsync', () => {
|
|
259
|
-
it('
|
|
259
|
+
it('succeeds when one row updated', async () => {
|
|
260
260
|
const queryContext = instance(mock(EntityQueryContext));
|
|
261
|
-
const adapter = new TestEntityDatabaseAdapter({ updateResults:
|
|
262
|
-
|
|
263
|
-
expect(result).toEqual({ stringField: 'hello' });
|
|
261
|
+
const adapter = new TestEntityDatabaseAdapter({ updateResults: { updatedRowCount: 1 } });
|
|
262
|
+
await adapter.updateAsync(queryContext, 'customIdField', 'wat', {});
|
|
264
263
|
});
|
|
265
264
|
|
|
266
265
|
it('throws when update result count zero', async () => {
|
|
267
266
|
const queryContext = instance(mock(EntityQueryContext));
|
|
268
|
-
const adapter = new TestEntityDatabaseAdapter({ updateResults:
|
|
267
|
+
const adapter = new TestEntityDatabaseAdapter({ updateResults: { updatedRowCount: 0 } });
|
|
269
268
|
await expect(adapter.updateAsync(queryContext, 'customIdField', 'wat', {})).rejects.toThrow(
|
|
270
269
|
EntityDatabaseAdapterEmptyUpdateResultError,
|
|
271
270
|
);
|
|
@@ -274,7 +273,7 @@ describe(EntityDatabaseAdapter, () => {
|
|
|
274
273
|
it('throws when update result count greater than 1', async () => {
|
|
275
274
|
const queryContext = instance(mock(EntityQueryContext));
|
|
276
275
|
const adapter = new TestEntityDatabaseAdapter({
|
|
277
|
-
updateResults:
|
|
276
|
+
updateResults: { updatedRowCount: 2 },
|
|
278
277
|
});
|
|
279
278
|
await expect(adapter.updateAsync(queryContext, 'customIdField', 'wat', {})).rejects.toThrow(
|
|
280
279
|
EntityDatabaseAdapterExcessiveUpdateResultError,
|
|
@@ -203,6 +203,90 @@ describe(EntityQueryContext, () => {
|
|
|
203
203
|
},
|
|
204
204
|
);
|
|
205
205
|
});
|
|
206
|
+
|
|
207
|
+
it('exposes isolationLevel on the query context', async () => {
|
|
208
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
209
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
210
|
+
|
|
211
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
212
|
+
'postgres',
|
|
213
|
+
async (queryContext) => {
|
|
214
|
+
assert(queryContext.isInTransaction());
|
|
215
|
+
expect(queryContext.isolationLevel).toBe(TransactionIsolationLevel.SERIALIZABLE);
|
|
216
|
+
},
|
|
217
|
+
{ isolationLevel: TransactionIsolationLevel.SERIALIZABLE },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
221
|
+
'postgres',
|
|
222
|
+
async (queryContext) => {
|
|
223
|
+
assert(queryContext.isInTransaction());
|
|
224
|
+
expect(queryContext.isolationLevel).toBe(TransactionIsolationLevel.REPEATABLE_READ);
|
|
225
|
+
},
|
|
226
|
+
{ isolationLevel: TransactionIsolationLevel.REPEATABLE_READ },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
230
|
+
'postgres',
|
|
231
|
+
async (queryContext) => {
|
|
232
|
+
assert(queryContext.isInTransaction());
|
|
233
|
+
expect(queryContext.isolationLevel).toBeUndefined();
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('exposes the full resolved transactionConfig on the query context', async () => {
|
|
239
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
240
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
241
|
+
|
|
242
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
243
|
+
'postgres',
|
|
244
|
+
async (queryContext) => {
|
|
245
|
+
assert(queryContext.isInTransaction());
|
|
246
|
+
expect(queryContext.transactionConfig).toEqual({
|
|
247
|
+
isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
|
|
248
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
|
|
253
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED,
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
258
|
+
'postgres',
|
|
259
|
+
async (queryContext) => {
|
|
260
|
+
assert(queryContext.isInTransaction());
|
|
261
|
+
expect(queryContext.transactionConfig).toEqual({
|
|
262
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode.ENABLED,
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('propagates transactionConfig to nested transactions', async () => {
|
|
269
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
270
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
271
|
+
|
|
272
|
+
await viewerContext.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
273
|
+
'postgres',
|
|
274
|
+
async (queryContext) => {
|
|
275
|
+
assert(queryContext.isInTransaction());
|
|
276
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
277
|
+
expect(innerQueryContext.transactionConfig).toEqual(queryContext.transactionConfig);
|
|
278
|
+
expect(innerQueryContext.isolationLevel).toBe(TransactionIsolationLevel.SERIALIZABLE);
|
|
279
|
+
expect(innerQueryContext.transactionalDataLoaderMode).toBe(
|
|
280
|
+
TransactionalDataLoaderMode.DISABLED,
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
|
|
286
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
});
|
|
206
290
|
});
|
|
207
291
|
|
|
208
292
|
describe('global defaultTransactionalDataLoaderMode', () => {
|
|
@@ -34,6 +34,9 @@ export class EntityInvalidFieldValueError<
|
|
|
34
34
|
return EntityErrorCode.ERR_ENTITY_INVALID_FIELD_VALUE;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
readonly fieldName: N;
|
|
38
|
+
readonly fieldValue: TFields[N] | undefined;
|
|
39
|
+
|
|
37
40
|
constructor(
|
|
38
41
|
entityClass: IEntityClass<
|
|
39
42
|
TFields,
|
|
@@ -47,5 +50,7 @@ export class EntityInvalidFieldValueError<
|
|
|
47
50
|
fieldValue?: TFields[N],
|
|
48
51
|
) {
|
|
49
52
|
super(`Entity field not valid: ${entityClass.name} (${String(fieldName)} = ${fieldValue})`);
|
|
53
|
+
this.fieldName = fieldName;
|
|
54
|
+
this.fieldValue = fieldValue;
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -34,6 +34,8 @@ describe('EntityError subclasses', () => {
|
|
|
34
34
|
const error = new EntityInvalidFieldValueError(SimpleTestEntity, 'id', 'badValue');
|
|
35
35
|
expect(error.state).toBe(EntityErrorState.PERMANENT);
|
|
36
36
|
expect(error.code).toBe(EntityErrorCode.ERR_ENTITY_INVALID_FIELD_VALUE);
|
|
37
|
+
expect(error.fieldName).toBe('id');
|
|
38
|
+
expect(error.fieldValue).toBe('badValue');
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
it('EntityCacheAdapterTransientError has correct state and code', () => {
|
|
@@ -4,12 +4,29 @@ import type {
|
|
|
4
4
|
} from '../EntityPrivacyPolicy.ts';
|
|
5
5
|
import type { EntityLoadMethodType } from '../internal/EntityLoadInterfaces.ts';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The type of the load method being called.
|
|
9
|
+
*/
|
|
7
10
|
export enum EntityMetricsLoadType {
|
|
11
|
+
/**
|
|
12
|
+
* Standard EntityLoader load (the methods from EnforcingEntityLoader or AuthorizationResultBasedEntityLoader).
|
|
13
|
+
*/
|
|
8
14
|
LOAD_MANY,
|
|
15
|
+
/**
|
|
16
|
+
* Knex loader load using loadManyByFieldEqualityConjunctionAsync.
|
|
17
|
+
*/
|
|
9
18
|
LOAD_MANY_EQUALITY_CONJUNCTION,
|
|
10
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Knex loader load using loadManyBySQL.
|
|
21
|
+
*/
|
|
11
22
|
LOAD_MANY_SQL,
|
|
23
|
+
/**
|
|
24
|
+
* Internal data manager load via database adapter method loadOneEqualingAsync.
|
|
25
|
+
*/
|
|
12
26
|
LOAD_ONE,
|
|
27
|
+
/**
|
|
28
|
+
* Knex loader load using loadPageAsync.
|
|
29
|
+
*/
|
|
13
30
|
LOAD_PAGE,
|
|
14
31
|
}
|
|
15
32
|
|
|
@@ -43,6 +60,9 @@ export interface EntityMetricsLoadEvent {
|
|
|
43
60
|
count: number;
|
|
44
61
|
}
|
|
45
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The type of mutation being performed.
|
|
65
|
+
*/
|
|
46
66
|
export enum EntityMetricsMutationType {
|
|
47
67
|
CREATE,
|
|
48
68
|
UPDATE,
|
|
@@ -71,6 +91,9 @@ export interface EntityMetricsMutationEvent {
|
|
|
71
91
|
duration: number;
|
|
72
92
|
}
|
|
73
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Type used to delineate dataloader, cache, and database load counts in EntityDataManager.
|
|
96
|
+
*/
|
|
74
97
|
export enum IncrementLoadCountEventType {
|
|
75
98
|
/**
|
|
76
99
|
* Type for when a dataloader load is initiated via the standard load methods
|
|
@@ -135,7 +135,7 @@ export class StubDatabaseAdapter<
|
|
|
135
135
|
tableIdField: string,
|
|
136
136
|
id: any,
|
|
137
137
|
object: object,
|
|
138
|
-
): Promise<
|
|
138
|
+
): Promise<{ updatedRowCount: number }> {
|
|
139
139
|
// SQL does not support empty updates, mirror behavior here for better test simulation
|
|
140
140
|
if (Object.keys(object).length === 0) {
|
|
141
141
|
throw new Error(`Empty update (${tableIdField} = ${id})`);
|
|
@@ -150,14 +150,14 @@ export class StubDatabaseAdapter<
|
|
|
150
150
|
// SQL updates to a nonexistent row succeed but affect 0 rows,
|
|
151
151
|
// mirror that behavior here for better test simulation
|
|
152
152
|
if (objectIndex < 0) {
|
|
153
|
-
return
|
|
153
|
+
return { updatedRowCount: 0 };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
objectCollection[objectIndex] = {
|
|
157
157
|
...objectCollection[objectIndex],
|
|
158
158
|
...object,
|
|
159
159
|
};
|
|
160
|
-
return
|
|
160
|
+
return { updatedRowCount: 1 };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
protected async deleteInternalAsync(
|