@expo/entity-database-adapter-knex 0.43.0 → 0.45.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity-database-adapter-knex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Knex database adapter for @expo/entity",
|
|
5
5
|
"files": [
|
|
6
6
|
"build",
|
|
@@ -28,24 +28,24 @@
|
|
|
28
28
|
"author": "Expo",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@expo/entity": "^0.
|
|
31
|
+
"@expo/entity": "^0.45.0",
|
|
32
32
|
"knex": "^3.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@expo/entity-testing-utils": "^0.
|
|
35
|
+
"@expo/entity-testing-utils": "^0.45.0",
|
|
36
36
|
"@types/jest": "^29.5.14",
|
|
37
37
|
"@types/node": "^20.14.1",
|
|
38
38
|
"ctix": "^2.7.0",
|
|
39
|
-
"eslint": "^
|
|
40
|
-
"eslint-config-universe": "^
|
|
41
|
-
"eslint-plugin-tsdoc": "^0.
|
|
39
|
+
"eslint": "^9.26.0",
|
|
40
|
+
"eslint-config-universe": "^15.0.3",
|
|
41
|
+
"eslint-plugin-tsdoc": "^0.4.0",
|
|
42
42
|
"jest": "^29.7.0",
|
|
43
43
|
"pg": "8.14.1",
|
|
44
|
-
"prettier": "^3.
|
|
44
|
+
"prettier": "^3.5.3",
|
|
45
45
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
46
|
-
"ts-jest": "^29.3.
|
|
46
|
+
"ts-jest": "^29.3.2",
|
|
47
47
|
"ts-mockito": "^2.6.1",
|
|
48
48
|
"typescript": "^5.8.3"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "fe2d246f87adc98a13cc7c1ae57f3628769560c9"
|
|
51
51
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createWithUniqueConstraintRecoveryAsync, ViewerContext } from '@expo/entity';
|
|
2
|
+
import knex, { Knex } from 'knex';
|
|
3
|
+
import nullthrows from 'nullthrows';
|
|
4
|
+
|
|
5
|
+
import PostgresUniqueTestEntity from '../__testfixtures__/PostgresUniqueTestEntity';
|
|
6
|
+
import { createKnexIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createKnexIntegrationTestEntityCompanionProvider';
|
|
7
|
+
|
|
8
|
+
describe(createWithUniqueConstraintRecoveryAsync, () => {
|
|
9
|
+
let knexInstance: Knex;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
knexInstance = knex({
|
|
13
|
+
client: 'pg',
|
|
14
|
+
connection: {
|
|
15
|
+
user: nullthrows(process.env['PGUSER']),
|
|
16
|
+
password: nullthrows(process.env['PGPASSWORD']),
|
|
17
|
+
host: 'localhost',
|
|
18
|
+
port: parseInt(nullthrows(process.env['PGPORT']), 10),
|
|
19
|
+
database: nullthrows(process.env['PGDATABASE']),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
await PostgresUniqueTestEntity.createOrTruncatePostgresTableAsync(knexInstance);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
await PostgresUniqueTestEntity.dropPostgresTableAsync(knexInstance);
|
|
30
|
+
await knexInstance.destroy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe.each([true, false])('is parallel creations %p', (parallel) => {
|
|
34
|
+
it('recovers when the same entity is created twice outside of transaction', async () => {
|
|
35
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
36
|
+
|
|
37
|
+
const args = {
|
|
38
|
+
name: 'unique',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let createdEntities: [PostgresUniqueTestEntity, PostgresUniqueTestEntity];
|
|
42
|
+
if (parallel) {
|
|
43
|
+
createdEntities = await Promise.all([
|
|
44
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
45
|
+
vc1,
|
|
46
|
+
PostgresUniqueTestEntity,
|
|
47
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
48
|
+
args,
|
|
49
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
50
|
+
args,
|
|
51
|
+
),
|
|
52
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
53
|
+
vc1,
|
|
54
|
+
PostgresUniqueTestEntity,
|
|
55
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
56
|
+
args,
|
|
57
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
58
|
+
args,
|
|
59
|
+
),
|
|
60
|
+
]);
|
|
61
|
+
} else {
|
|
62
|
+
createdEntities = [
|
|
63
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
64
|
+
vc1,
|
|
65
|
+
PostgresUniqueTestEntity,
|
|
66
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
67
|
+
args,
|
|
68
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
69
|
+
args,
|
|
70
|
+
),
|
|
71
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
72
|
+
vc1,
|
|
73
|
+
PostgresUniqueTestEntity,
|
|
74
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
75
|
+
args,
|
|
76
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
77
|
+
args,
|
|
78
|
+
),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(createdEntities[0].getID()).toEqual(createdEntities[1].getID());
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('recovers when the same entity is created twice within same transaction', async () => {
|
|
86
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
87
|
+
|
|
88
|
+
const args = {
|
|
89
|
+
name: 'unique',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const createdEntities = await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
93
|
+
'postgres',
|
|
94
|
+
async (queryContext) => {
|
|
95
|
+
if (parallel) {
|
|
96
|
+
return await Promise.all([
|
|
97
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
98
|
+
vc1,
|
|
99
|
+
PostgresUniqueTestEntity,
|
|
100
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
101
|
+
args,
|
|
102
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
103
|
+
args,
|
|
104
|
+
queryContext,
|
|
105
|
+
),
|
|
106
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
107
|
+
vc1,
|
|
108
|
+
PostgresUniqueTestEntity,
|
|
109
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
110
|
+
args,
|
|
111
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
112
|
+
args,
|
|
113
|
+
queryContext,
|
|
114
|
+
),
|
|
115
|
+
]);
|
|
116
|
+
} else {
|
|
117
|
+
return [
|
|
118
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
119
|
+
vc1,
|
|
120
|
+
PostgresUniqueTestEntity,
|
|
121
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
122
|
+
args,
|
|
123
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
124
|
+
args,
|
|
125
|
+
queryContext,
|
|
126
|
+
),
|
|
127
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
128
|
+
vc1,
|
|
129
|
+
PostgresUniqueTestEntity,
|
|
130
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
131
|
+
args,
|
|
132
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
133
|
+
args,
|
|
134
|
+
queryContext,
|
|
135
|
+
),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(nullthrows(createdEntities[0]).getID()).toEqual(
|
|
142
|
+
nullthrows(createdEntities[1]).getID(),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('recovers when the same entity is created twice within two transactions', async () => {
|
|
147
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
148
|
+
|
|
149
|
+
const args = {
|
|
150
|
+
name: 'unique',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let createdEntities: [PostgresUniqueTestEntity, PostgresUniqueTestEntity];
|
|
154
|
+
if (parallel) {
|
|
155
|
+
createdEntities = await Promise.all([
|
|
156
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
157
|
+
'postgres',
|
|
158
|
+
async (queryContext) => {
|
|
159
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
160
|
+
vc1,
|
|
161
|
+
PostgresUniqueTestEntity,
|
|
162
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
163
|
+
args,
|
|
164
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
165
|
+
args,
|
|
166
|
+
queryContext,
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
171
|
+
'postgres',
|
|
172
|
+
async (queryContext) => {
|
|
173
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
174
|
+
vc1,
|
|
175
|
+
PostgresUniqueTestEntity,
|
|
176
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
177
|
+
args,
|
|
178
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
179
|
+
args,
|
|
180
|
+
queryContext,
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
]);
|
|
185
|
+
} else {
|
|
186
|
+
createdEntities = [
|
|
187
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
188
|
+
'postgres',
|
|
189
|
+
async (queryContext) => {
|
|
190
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
191
|
+
vc1,
|
|
192
|
+
PostgresUniqueTestEntity,
|
|
193
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
194
|
+
args,
|
|
195
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
196
|
+
args,
|
|
197
|
+
queryContext,
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
),
|
|
201
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
202
|
+
'postgres',
|
|
203
|
+
async (queryContext) => {
|
|
204
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
205
|
+
vc1,
|
|
206
|
+
PostgresUniqueTestEntity,
|
|
207
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
208
|
+
args,
|
|
209
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
210
|
+
args,
|
|
211
|
+
queryContext,
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
),
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expect(nullthrows(createdEntities[0]).getID()).toEqual(
|
|
219
|
+
nullthrows(createdEntities[1]).getID(),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ViewerContext } from '@expo/entity';
|
|
1
|
+
import { TransactionalDataLoaderMode, ViewerContext } from '@expo/entity';
|
|
2
2
|
import { knex, Knex } from 'knex';
|
|
3
3
|
import nullthrows from 'nullthrows';
|
|
4
4
|
|
|
@@ -47,7 +47,7 @@ describe(PostgresEntityQueryContextProvider, () => {
|
|
|
47
47
|
.updateAsync();
|
|
48
48
|
|
|
49
49
|
// ensure the outer transaction is not aborted due to postgres error in inner transaction,
|
|
50
|
-
// in this case the error triggered is a unique constraint violation
|
|
50
|
+
// in this case the error triggered is a unique constraint violation from a conflict with the first entity created above
|
|
51
51
|
try {
|
|
52
52
|
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
53
53
|
const entity = await PostgresUniqueTestEntity.loader(
|
|
@@ -70,6 +70,360 @@ describe(PostgresEntityQueryContextProvider, () => {
|
|
|
70
70
|
expect(entityLoaded.getField('name')).toEqual('wat3');
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
test('dataloader consistency with nested transactions', async () => {
|
|
74
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
75
|
+
|
|
76
|
+
// put it in local dataloader
|
|
77
|
+
const entity = await PostgresUniqueTestEntity.creator(vc1)
|
|
78
|
+
.setField('name', 'who')
|
|
79
|
+
.createAsync();
|
|
80
|
+
const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
|
|
81
|
+
expect(entityLoaded.getField('name')).toEqual('who');
|
|
82
|
+
|
|
83
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
|
|
84
|
+
const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
85
|
+
entity.getID(),
|
|
86
|
+
);
|
|
87
|
+
expect(entityLoadedOuter.getField('name')).toEqual('who');
|
|
88
|
+
|
|
89
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
90
|
+
const entityLoadedInner = await PostgresUniqueTestEntity.loader(
|
|
91
|
+
vc1,
|
|
92
|
+
innerQueryContext,
|
|
93
|
+
).loadByIDAsync(entity.getID());
|
|
94
|
+
const updatedEntity = await PostgresUniqueTestEntity.updater(
|
|
95
|
+
entityLoadedInner,
|
|
96
|
+
innerQueryContext,
|
|
97
|
+
)
|
|
98
|
+
.setField('name', 'wat')
|
|
99
|
+
.updateAsync();
|
|
100
|
+
expect(updatedEntity.getField('name')).toEqual('wat');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const entityLoadedAfterNested = await PostgresUniqueTestEntity.loader(
|
|
104
|
+
vc1,
|
|
105
|
+
queryContext,
|
|
106
|
+
).loadByIDAsync(entity.getID());
|
|
107
|
+
expect(entityLoadedAfterNested.getField('name')).toEqual('wat');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
111
|
+
entity.getID(),
|
|
112
|
+
);
|
|
113
|
+
expect(entityLoadedAfterTransaction.getField('name')).toEqual('wat');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('dataloader consistency with nested transactions that throw', async () => {
|
|
117
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
118
|
+
|
|
119
|
+
// put it in local dataloader
|
|
120
|
+
const entity = await PostgresUniqueTestEntity.creator(vc1)
|
|
121
|
+
.setField('name', 'who')
|
|
122
|
+
.createAsync();
|
|
123
|
+
const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
|
|
124
|
+
expect(entityLoaded.getField('name')).toEqual('who');
|
|
125
|
+
|
|
126
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
|
|
127
|
+
const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
128
|
+
entity.getID(),
|
|
129
|
+
);
|
|
130
|
+
expect(entityLoadedOuter.getField('name')).toEqual('who');
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
134
|
+
const entityLoadedInner = await PostgresUniqueTestEntity.loader(
|
|
135
|
+
vc1,
|
|
136
|
+
innerQueryContext,
|
|
137
|
+
).loadByIDAsync(entity.getID());
|
|
138
|
+
const updatedEntity = await PostgresUniqueTestEntity.updater(
|
|
139
|
+
entityLoadedInner,
|
|
140
|
+
innerQueryContext,
|
|
141
|
+
)
|
|
142
|
+
.setField('name', 'wat')
|
|
143
|
+
.updateAsync();
|
|
144
|
+
expect(updatedEntity.getField('name')).toEqual('wat');
|
|
145
|
+
throw new Error('wat');
|
|
146
|
+
});
|
|
147
|
+
} catch {}
|
|
148
|
+
|
|
149
|
+
const entityLoadedAfterNested = await PostgresUniqueTestEntity.loader(
|
|
150
|
+
vc1,
|
|
151
|
+
queryContext,
|
|
152
|
+
).loadByIDAsync(entity.getID());
|
|
153
|
+
expect(entityLoadedAfterNested.getField('name')).toEqual('who');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
157
|
+
entity.getID(),
|
|
158
|
+
);
|
|
159
|
+
expect(entityLoadedAfterTransaction.getField('name')).toEqual('who');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('dataloader consistency with concurrent loads outside of transaction', async () => {
|
|
163
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
164
|
+
|
|
165
|
+
// put it in local dataloader
|
|
166
|
+
const entity = await PostgresUniqueTestEntity.creator(vc1)
|
|
167
|
+
.setField('name', 'who')
|
|
168
|
+
.createAsync();
|
|
169
|
+
const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
|
|
170
|
+
expect(entityLoaded.getField('name')).toEqual('who');
|
|
171
|
+
|
|
172
|
+
let openBarrier1: () => void;
|
|
173
|
+
const barrier1 = new Promise<void>((resolve) => {
|
|
174
|
+
openBarrier1 = resolve;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let openBarrier2: () => void;
|
|
178
|
+
const barrier2 = new Promise<void>((resolve) => {
|
|
179
|
+
openBarrier2 = resolve;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await Promise.all([
|
|
183
|
+
vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
|
|
184
|
+
const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
185
|
+
entity.getID(),
|
|
186
|
+
);
|
|
187
|
+
expect(entityLoadedOuter.getField('name')).toEqual('who');
|
|
188
|
+
|
|
189
|
+
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
190
|
+
const entityLoadedInner = await PostgresUniqueTestEntity.loader(
|
|
191
|
+
vc1,
|
|
192
|
+
innerQueryContext,
|
|
193
|
+
).loadByIDAsync(entity.getID());
|
|
194
|
+
const updatedEntity = await PostgresUniqueTestEntity.updater(
|
|
195
|
+
entityLoadedInner,
|
|
196
|
+
innerQueryContext,
|
|
197
|
+
)
|
|
198
|
+
.setField('name', 'wat')
|
|
199
|
+
.updateAsync();
|
|
200
|
+
expect(updatedEntity.getField('name')).toEqual('wat');
|
|
201
|
+
const entityLoadedAfterUpdate = await PostgresUniqueTestEntity.loader(
|
|
202
|
+
vc1,
|
|
203
|
+
innerQueryContext,
|
|
204
|
+
).loadByIDAsync(entity.getID());
|
|
205
|
+
expect(entityLoadedAfterUpdate.getField('name')).toEqual('wat');
|
|
206
|
+
});
|
|
207
|
+
openBarrier1();
|
|
208
|
+
await barrier2;
|
|
209
|
+
}),
|
|
210
|
+
(async () => {
|
|
211
|
+
await barrier1;
|
|
212
|
+
|
|
213
|
+
const entityLoadedOutsideOfTransactionsBeforeNestedCommit =
|
|
214
|
+
await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
|
|
215
|
+
expect(entityLoadedOutsideOfTransactionsBeforeNestedCommit.getField('name')).toEqual('who');
|
|
216
|
+
openBarrier2!();
|
|
217
|
+
})(),
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
|
|
221
|
+
entity.getID(),
|
|
222
|
+
);
|
|
223
|
+
expect(entityLoadedAfterTransaction.getField('name')).toEqual('wat');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('consistent behavior with and without transactional dataloader for concurrent loads outside of nested transaction', async () => {
|
|
227
|
+
// Subtransactions are not supported in postgres. See #194 for more info.
|
|
228
|
+
// Instead, savepoints and rollbacks are used to simulate subtransactions, which results in non-isolated read semantics.
|
|
229
|
+
//
|
|
230
|
+
// This test tests the same behavior exists whether there is a dataloader or not in transactions, thus indicating that
|
|
231
|
+
// the dataloader invalidation is not the issue.
|
|
232
|
+
|
|
233
|
+
const runTest = async (
|
|
234
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode,
|
|
235
|
+
): Promise<void> => {
|
|
236
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
237
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
238
|
+
'postgres',
|
|
239
|
+
async (outerQueryContext) => {
|
|
240
|
+
// put it in local dataloader
|
|
241
|
+
const entity = await PostgresUniqueTestEntity.creator(vc1, outerQueryContext)
|
|
242
|
+
.setField('name', 'who')
|
|
243
|
+
.createAsync();
|
|
244
|
+
const entityLoaded = await PostgresUniqueTestEntity.loader(
|
|
245
|
+
vc1,
|
|
246
|
+
outerQueryContext,
|
|
247
|
+
).loadByIDAsync(entity.getID());
|
|
248
|
+
if (entityLoaded.getField('name') !== 'who') {
|
|
249
|
+
throw new Error('entity loaded wrong value');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let openBarrier1: () => void;
|
|
253
|
+
const barrier1 = new Promise<void>((resolve) => {
|
|
254
|
+
openBarrier1 = resolve;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
let openBarrier2: () => void;
|
|
258
|
+
const barrier2 = new Promise<void>((resolve) => {
|
|
259
|
+
openBarrier2 = resolve;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await Promise.all([
|
|
263
|
+
outerQueryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
264
|
+
const entityLoadedInner = await PostgresUniqueTestEntity.loader(
|
|
265
|
+
vc1,
|
|
266
|
+
innerQueryContext,
|
|
267
|
+
).loadByIDAsync(entity.getID());
|
|
268
|
+
const updatedEntity = await PostgresUniqueTestEntity.updater(
|
|
269
|
+
entityLoadedInner,
|
|
270
|
+
innerQueryContext,
|
|
271
|
+
)
|
|
272
|
+
.setField('name', 'wat')
|
|
273
|
+
.updateAsync();
|
|
274
|
+
if (updatedEntity.getField('name') !== 'wat') {
|
|
275
|
+
throw new Error('entity updated wrong value');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const entityLoadedAfterUpdate = await PostgresUniqueTestEntity.loader(
|
|
279
|
+
vc1,
|
|
280
|
+
innerQueryContext,
|
|
281
|
+
).loadByIDAsync(entity.getID());
|
|
282
|
+
if (entityLoadedAfterUpdate.getField('name') !== 'wat') {
|
|
283
|
+
throw new Error('entity loaded wrong value after update');
|
|
284
|
+
}
|
|
285
|
+
openBarrier1();
|
|
286
|
+
await barrier2;
|
|
287
|
+
}),
|
|
288
|
+
(async () => {
|
|
289
|
+
await barrier1;
|
|
290
|
+
|
|
291
|
+
// if postgres supported nested transactions, this would read isolated from the nested transaction above
|
|
292
|
+
// but since it doesn't, this will read the updated value.
|
|
293
|
+
const entityLoadedInOuterTransactionBeforeNestedCommit =
|
|
294
|
+
await PostgresUniqueTestEntity.loader(vc1, outerQueryContext).loadByIDAsync(
|
|
295
|
+
entity.getID(),
|
|
296
|
+
);
|
|
297
|
+
if (entityLoadedInOuterTransactionBeforeNestedCommit.getField('name') !== 'who') {
|
|
298
|
+
throw new Error('outer transaction read wrong value');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
openBarrier2!();
|
|
302
|
+
})(),
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(
|
|
306
|
+
vc1,
|
|
307
|
+
outerQueryContext,
|
|
308
|
+
).loadByIDAsync(entity.getID());
|
|
309
|
+
if (entityLoadedAfterTransaction.getField('name') !== 'wat') {
|
|
310
|
+
throw new Error('entity loaded wrong value after transaction');
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{ transactionalDataLoaderMode },
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await expect(runTest(TransactionalDataLoaderMode.DISABLED)).rejects.toThrow(
|
|
318
|
+
'outer transaction read wrong value',
|
|
319
|
+
);
|
|
320
|
+
await expect(runTest(TransactionalDataLoaderMode.ENABLED)).rejects.toThrow(
|
|
321
|
+
'outer transaction read wrong value',
|
|
322
|
+
);
|
|
323
|
+
await expect(runTest(TransactionalDataLoaderMode.ENABLED_BATCH_ONLY)).rejects.toThrow(
|
|
324
|
+
'outer transaction read wrong value',
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('consistent behavior with and without transactional dataloader for concurrent mutations outside of nested transaction that reads', async () => {
|
|
329
|
+
// this test has a similar issue to the one above: absence of real nested transactions in postgres
|
|
330
|
+
// this should have the same behavior no matter if there is a dataloader or not in transactions
|
|
331
|
+
|
|
332
|
+
const runTest = async (
|
|
333
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode,
|
|
334
|
+
): Promise<void> => {
|
|
335
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
336
|
+
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
337
|
+
'postgres',
|
|
338
|
+
async (outerQueryContext) => {
|
|
339
|
+
// put it in local dataloader
|
|
340
|
+
const entity = await PostgresUniqueTestEntity.creator(vc1, outerQueryContext)
|
|
341
|
+
.setField('name', 'who')
|
|
342
|
+
.createAsync();
|
|
343
|
+
const entityLoaded = await PostgresUniqueTestEntity.loader(
|
|
344
|
+
vc1,
|
|
345
|
+
outerQueryContext,
|
|
346
|
+
).loadByIDAsync(entity.getID());
|
|
347
|
+
if (entityLoaded.getField('name') !== 'who') {
|
|
348
|
+
throw new Error('entity loaded wrong value');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let openBarrier1: () => void;
|
|
352
|
+
const barrier1 = new Promise<void>((resolve) => {
|
|
353
|
+
openBarrier1 = resolve;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
let openBarrier2: () => void;
|
|
357
|
+
const barrier2 = new Promise<void>((resolve) => {
|
|
358
|
+
openBarrier2 = resolve;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await Promise.all([
|
|
362
|
+
(async () => {
|
|
363
|
+
await barrier1;
|
|
364
|
+
|
|
365
|
+
const entityLoadedOuterAgain = await PostgresUniqueTestEntity.loader(
|
|
366
|
+
vc1,
|
|
367
|
+
outerQueryContext,
|
|
368
|
+
).loadByIDAsync(entity.getID());
|
|
369
|
+
const updatedEntity = await PostgresUniqueTestEntity.updater(
|
|
370
|
+
entityLoadedOuterAgain,
|
|
371
|
+
outerQueryContext,
|
|
372
|
+
)
|
|
373
|
+
.setField('name', 'wat')
|
|
374
|
+
.updateAsync();
|
|
375
|
+
if (updatedEntity.getField('name') !== 'wat') {
|
|
376
|
+
throw new Error('entity updated wrong value');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
openBarrier2!();
|
|
380
|
+
})(),
|
|
381
|
+
|
|
382
|
+
outerQueryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
383
|
+
const entityLoadedInner = await PostgresUniqueTestEntity.loader(
|
|
384
|
+
vc1,
|
|
385
|
+
innerQueryContext,
|
|
386
|
+
).loadByIDAsync(entity.getID());
|
|
387
|
+
if (entityLoadedInner.getField('name') !== 'who') {
|
|
388
|
+
throw new Error('entity loaded inner wrong value 1');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
openBarrier1();
|
|
392
|
+
await barrier2;
|
|
393
|
+
|
|
394
|
+
const entityLoadedInnerAgain = await PostgresUniqueTestEntity.loader(
|
|
395
|
+
vc1,
|
|
396
|
+
innerQueryContext,
|
|
397
|
+
).loadByIDAsync(entity.getID());
|
|
398
|
+
if (entityLoadedInnerAgain.getField('name') !== 'who') {
|
|
399
|
+
throw new Error('entity loaded inner wrong value 2');
|
|
400
|
+
}
|
|
401
|
+
}),
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(
|
|
405
|
+
vc1,
|
|
406
|
+
outerQueryContext,
|
|
407
|
+
).loadByIDAsync(entity.getID());
|
|
408
|
+
if (entityLoadedAfterTransaction.getField('name') !== 'wat') {
|
|
409
|
+
throw new Error('entity loaded wrong value after transaction');
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{ transactionalDataLoaderMode },
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
await expect(runTest(TransactionalDataLoaderMode.DISABLED)).rejects.toThrow(
|
|
417
|
+
'entity loaded inner wrong value 2',
|
|
418
|
+
);
|
|
419
|
+
await expect(runTest(TransactionalDataLoaderMode.ENABLED)).rejects.toThrow(
|
|
420
|
+
'entity loaded inner wrong value 2',
|
|
421
|
+
);
|
|
422
|
+
await expect(runTest(TransactionalDataLoaderMode.ENABLED_BATCH_ONLY)).rejects.toThrow(
|
|
423
|
+
'entity loaded inner wrong value 2',
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
73
427
|
it('supports multi-nested transactions', async () => {
|
|
74
428
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
75
429
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
EntityCompanionDefinition,
|
|
8
8
|
Entity,
|
|
9
9
|
UUIDField,
|
|
10
|
+
EntityTransactionalQueryContext,
|
|
10
11
|
} from '@expo/entity';
|
|
11
12
|
import { Knex } from 'knex';
|
|
12
13
|
|
|
@@ -53,6 +54,27 @@ export default class PostgresUniqueTestEntity extends Entity<
|
|
|
53
54
|
await knex.schema.dropTable(tableName);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
|
|
58
|
+
public static async getByNameAsync(
|
|
59
|
+
viewerContext: ViewerContext,
|
|
60
|
+
args: { name: string },
|
|
61
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
62
|
+
): Promise<PostgresUniqueTestEntity | null> {
|
|
63
|
+
return await PostgresUniqueTestEntity.loader(
|
|
64
|
+
viewerContext,
|
|
65
|
+
queryContext,
|
|
66
|
+
).loadByFieldEqualingAsync('name', args.name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public static async createWithNameAsync(
|
|
70
|
+
viewerContext: ViewerContext,
|
|
71
|
+
args: { name: string },
|
|
72
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
73
|
+
): Promise<PostgresUniqueTestEntity> {
|
|
74
|
+
return await PostgresUniqueTestEntity.creator(viewerContext, queryContext)
|
|
75
|
+
.setField('name', args.name)
|
|
76
|
+
.createAsync();
|
|
77
|
+
}
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
class PostgresUniqueTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
@@ -108,6 +130,7 @@ export const postgresTestEntityConfiguration = new EntityConfiguration<
|
|
|
108
130
|
}),
|
|
109
131
|
name: new StringField({
|
|
110
132
|
columnName: 'name',
|
|
133
|
+
cache: true,
|
|
111
134
|
}),
|
|
112
135
|
},
|
|
113
136
|
databaseAdapterFlavor: 'postgres',
|