@geekmidas/testkit 0.0.16 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -199
- package/dist/{KyselyFactory-DRQ83r0o.d.cts → KyselyFactory-BFygzOlO.d.cts} +21 -8
- package/dist/{KyselyFactory-Cx3sezwH.d.mts → KyselyFactory-BTdygZ-i.d.mts} +21 -8
- package/dist/{KyselyFactory-BcYkC0t2.mjs → KyselyFactory-CXY5gJk2.mjs} +25 -12
- package/dist/KyselyFactory-CXY5gJk2.mjs.map +1 -0
- package/dist/{KyselyFactory-Cf0o2YxO.cjs → KyselyFactory-DaaCykWP.cjs} +25 -12
- package/dist/KyselyFactory-DaaCykWP.cjs.map +1 -0
- package/dist/KyselyFactory.cjs +1 -1
- package/dist/KyselyFactory.d.cts +1 -1
- package/dist/KyselyFactory.d.mts +1 -1
- package/dist/KyselyFactory.mjs +1 -1
- package/dist/{ObjectionFactory-C-59Hjwj.d.mts → ObjectionFactory-BagGjikT.d.mts} +24 -11
- package/dist/{ObjectionFactory-C4X78k0B.d.cts → ObjectionFactory-CeSIN3kZ.d.cts} +24 -11
- package/dist/{ObjectionFactory-CDriunkS.cjs → ObjectionFactory-Eb04AOnv.cjs} +28 -15
- package/dist/ObjectionFactory-Eb04AOnv.cjs.map +1 -0
- package/dist/{ObjectionFactory-8hebmnai.mjs → ObjectionFactory-zf2fLKrL.mjs} +28 -15
- package/dist/ObjectionFactory-zf2fLKrL.mjs.map +1 -0
- package/dist/ObjectionFactory.cjs +1 -1
- package/dist/ObjectionFactory.d.cts +1 -1
- package/dist/ObjectionFactory.d.mts +1 -1
- package/dist/ObjectionFactory.mjs +1 -1
- package/dist/{VitestKyselyTransactionIsolator-COCVfvfr.d.mts → VitestKyselyTransactionIsolator-4HOeLQ0d.d.cts} +2 -2
- package/dist/{VitestKyselyTransactionIsolator-Cst3vFjb.cjs → VitestKyselyTransactionIsolator-DX_VPKS-.cjs} +2 -2
- package/dist/{VitestKyselyTransactionIsolator-Cst3vFjb.cjs.map → VitestKyselyTransactionIsolator-DX_VPKS-.cjs.map} +1 -1
- package/dist/{VitestKyselyTransactionIsolator-DYUYVEh9.d.cts → VitestKyselyTransactionIsolator-DnyZMaA-.d.mts} +2 -2
- package/dist/{VitestKyselyTransactionIsolator-BxjlD1YM.mjs → VitestKyselyTransactionIsolator-XDL3ngs_.mjs} +2 -2
- package/dist/{VitestKyselyTransactionIsolator-BxjlD1YM.mjs.map → VitestKyselyTransactionIsolator-XDL3ngs_.mjs.map} +1 -1
- package/dist/VitestKyselyTransactionIsolator.cjs +2 -2
- package/dist/VitestKyselyTransactionIsolator.d.cts +2 -2
- package/dist/VitestKyselyTransactionIsolator.d.mts +2 -2
- package/dist/VitestKyselyTransactionIsolator.mjs +2 -2
- package/dist/{VitestObjectionTransactionIsolator-b973r9O1.d.mts → VitestObjectionTransactionIsolator-COVDlpEo.d.cts} +2 -2
- package/dist/{VitestObjectionTransactionIsolator-DzeF4UAq.cjs → VitestObjectionTransactionIsolator-D_tlOtq8.cjs} +2 -2
- package/dist/{VitestObjectionTransactionIsolator-DzeF4UAq.cjs.map → VitestObjectionTransactionIsolator-D_tlOtq8.cjs.map} +1 -1
- package/dist/{VitestObjectionTransactionIsolator-BU-jXEhz.mjs → VitestObjectionTransactionIsolator-_EhJKu_O.mjs} +2 -2
- package/dist/{VitestObjectionTransactionIsolator-BU-jXEhz.mjs.map → VitestObjectionTransactionIsolator-_EhJKu_O.mjs.map} +1 -1
- package/dist/{VitestObjectionTransactionIsolator-CJ4ds5Qv.d.cts → VitestObjectionTransactionIsolator-lZUSz1w0.d.mts} +2 -2
- package/dist/VitestObjectionTransactionIsolator.cjs +2 -2
- package/dist/VitestObjectionTransactionIsolator.d.cts +2 -2
- package/dist/VitestObjectionTransactionIsolator.d.mts +2 -2
- package/dist/VitestObjectionTransactionIsolator.mjs +2 -2
- package/dist/{VitestTransactionIsolator-CskiiJbW.mjs → VitestTransactionIsolator-BIaMs4c2.mjs} +40 -2
- package/dist/VitestTransactionIsolator-BIaMs4c2.mjs.map +1 -0
- package/dist/{VitestTransactionIsolator-BQ5FpLtC.cjs → VitestTransactionIsolator-BKIrj3Uy.cjs} +45 -1
- package/dist/VitestTransactionIsolator-BKIrj3Uy.cjs.map +1 -0
- package/dist/{VitestTransactionIsolator-CsfJBxcb.d.mts → VitestTransactionIsolator-CyG_i_Nj.d.cts} +61 -3
- package/dist/{VitestTransactionIsolator-DdLNODZg.d.cts → VitestTransactionIsolator-DWDbnITQ.d.mts} +61 -3
- package/dist/VitestTransactionIsolator.cjs +3 -2
- package/dist/VitestTransactionIsolator.d.cts +2 -2
- package/dist/VitestTransactionIsolator.d.mts +2 -2
- package/dist/VitestTransactionIsolator.mjs +2 -2
- package/dist/better-auth.cjs +242 -0
- package/dist/better-auth.cjs.map +1 -0
- package/dist/better-auth.d.cts +17 -0
- package/dist/better-auth.d.mts +17 -0
- package/dist/better-auth.mjs +241 -0
- package/dist/better-auth.mjs.map +1 -0
- package/dist/{directory-B4oYx02C.d.mts → directory-BXavAeJZ.d.mts} +3 -3
- package/dist/{directory-BUcnztHI.d.cts → directory-DlkPEzL4.d.cts} +3 -3
- package/dist/kysely.cjs +58 -4
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +58 -5
- package/dist/kysely.d.mts +58 -5
- package/dist/kysely.mjs +57 -5
- package/dist/kysely.mjs.map +1 -1
- package/dist/objection.cjs +54 -4
- package/dist/objection.cjs.map +1 -1
- package/dist/objection.d.cts +54 -5
- package/dist/objection.d.mts +54 -5
- package/dist/objection.mjs +53 -5
- package/dist/objection.mjs.map +1 -1
- package/dist/os/directory.d.cts +1 -1
- package/dist/os/directory.d.mts +1 -1
- package/dist/os/index.d.cts +1 -1
- package/dist/os/index.d.mts +1 -1
- package/package.json +13 -3
- package/src/KyselyFactory.ts +29 -16
- package/src/ObjectionFactory.ts +34 -19
- package/src/VitestTransactionIsolator.ts +110 -2
- package/src/__tests__/KyselyFactory.spec.ts +10 -10
- package/src/__tests__/ObjectionFactory.spec.ts +9 -12
- package/src/__tests__/better-auth.spec.ts +21 -0
- package/src/__tests__/integration.spec.ts +171 -14
- package/src/better-auth.ts +325 -0
- package/src/kysely.ts +66 -0
- package/src/objection.ts +61 -0
- package/dist/KyselyFactory-BcYkC0t2.mjs.map +0 -1
- package/dist/KyselyFactory-Cf0o2YxO.cjs.map +0 -1
- package/dist/ObjectionFactory-8hebmnai.mjs.map +0 -1
- package/dist/ObjectionFactory-CDriunkS.cjs.map +0 -1
- package/dist/VitestTransactionIsolator-BQ5FpLtC.cjs.map +0 -1
- package/dist/VitestTransactionIsolator-CskiiJbW.mjs.map +0 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DBAdapterDebugLogOption,
|
|
3
|
+
type Where,
|
|
4
|
+
createAdapterFactory,
|
|
5
|
+
} from 'better-auth/adapters';
|
|
6
|
+
|
|
7
|
+
interface MemoryAdapterConfig {
|
|
8
|
+
debugLogs?: DBAdapterDebugLogOption;
|
|
9
|
+
usePlural?: boolean;
|
|
10
|
+
initialData?: Record<string, any[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class MemoryStore {
|
|
14
|
+
constructor(
|
|
15
|
+
initialData?: Record<string, any[]>,
|
|
16
|
+
private readonly data: Map<string, any> = new Map(),
|
|
17
|
+
) {
|
|
18
|
+
if (initialData) {
|
|
19
|
+
for (const [model, records] of Object.entries(initialData)) {
|
|
20
|
+
const modelData = new Map();
|
|
21
|
+
for (const record of records) {
|
|
22
|
+
modelData.set(record.id, { ...record });
|
|
23
|
+
}
|
|
24
|
+
this.data.set(model, modelData);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getModel(modelName: string): Map<string, any> {
|
|
30
|
+
if (!this.data.has(modelName)) {
|
|
31
|
+
this.data.set(modelName, new Map());
|
|
32
|
+
}
|
|
33
|
+
return this.data.get(modelName)!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear() {
|
|
37
|
+
this.data.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getAllData() {
|
|
41
|
+
const result: Record<string, any[]> = {};
|
|
42
|
+
for (const [model, records] of this.data.entries()) {
|
|
43
|
+
result[model] = Array.from(records.values());
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function matchesWhere(record: any, where?: Where[]): boolean {
|
|
50
|
+
if (!where || where.length === 0) return true;
|
|
51
|
+
|
|
52
|
+
let result: boolean | null = null;
|
|
53
|
+
|
|
54
|
+
for (const condition of where) {
|
|
55
|
+
const { field, value, operator = 'eq', connector = 'AND' } = condition;
|
|
56
|
+
const recordValue = record[field];
|
|
57
|
+
|
|
58
|
+
let matches = false;
|
|
59
|
+
|
|
60
|
+
switch (operator) {
|
|
61
|
+
case 'eq':
|
|
62
|
+
matches = recordValue === value;
|
|
63
|
+
break;
|
|
64
|
+
case 'ne':
|
|
65
|
+
matches = recordValue !== value;
|
|
66
|
+
break;
|
|
67
|
+
case 'lt':
|
|
68
|
+
matches = value != null && recordValue < value;
|
|
69
|
+
break;
|
|
70
|
+
case 'lte':
|
|
71
|
+
matches = value != null && recordValue <= value;
|
|
72
|
+
break;
|
|
73
|
+
case 'gt':
|
|
74
|
+
matches = value != null && recordValue > value;
|
|
75
|
+
break;
|
|
76
|
+
case 'gte':
|
|
77
|
+
matches = value != null && recordValue >= value;
|
|
78
|
+
break;
|
|
79
|
+
case 'in':
|
|
80
|
+
matches =
|
|
81
|
+
Array.isArray(value) && (value as unknown[]).includes(recordValue);
|
|
82
|
+
break;
|
|
83
|
+
case 'not_in':
|
|
84
|
+
matches =
|
|
85
|
+
Array.isArray(value) && !(value as unknown[]).includes(recordValue);
|
|
86
|
+
break;
|
|
87
|
+
case 'contains':
|
|
88
|
+
matches =
|
|
89
|
+
typeof recordValue === 'string' &&
|
|
90
|
+
typeof value === 'string' &&
|
|
91
|
+
recordValue.includes(value);
|
|
92
|
+
break;
|
|
93
|
+
case 'starts_with':
|
|
94
|
+
matches =
|
|
95
|
+
typeof recordValue === 'string' &&
|
|
96
|
+
typeof value === 'string' &&
|
|
97
|
+
recordValue.startsWith(value);
|
|
98
|
+
break;
|
|
99
|
+
case 'ends_with':
|
|
100
|
+
matches =
|
|
101
|
+
typeof recordValue === 'string' &&
|
|
102
|
+
typeof value === 'string' &&
|
|
103
|
+
recordValue.endsWith(value);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
matches = recordValue === value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Apply connector logic
|
|
110
|
+
if (result === null) {
|
|
111
|
+
result = matches;
|
|
112
|
+
} else if (connector === 'OR') {
|
|
113
|
+
result = result || matches;
|
|
114
|
+
} else {
|
|
115
|
+
// Default: AND
|
|
116
|
+
result = result && matches;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result ?? true;
|
|
121
|
+
}
|
|
122
|
+
type SortBy = {
|
|
123
|
+
field: string;
|
|
124
|
+
direction: 'asc' | 'desc';
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function applySorting(records: any[], sortBy?: SortBy): any[] {
|
|
128
|
+
if (!sortBy) return records;
|
|
129
|
+
|
|
130
|
+
const { field, direction } = sortBy;
|
|
131
|
+
|
|
132
|
+
return records.sort((a, b) => {
|
|
133
|
+
const aVal = a[field];
|
|
134
|
+
const bVal = b[field];
|
|
135
|
+
|
|
136
|
+
if (aVal === bVal) return 0;
|
|
137
|
+
if (aVal == null) return direction === 'asc' ? -1 : 1;
|
|
138
|
+
if (bVal == null) return direction === 'asc' ? 1 : -1;
|
|
139
|
+
|
|
140
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
141
|
+
return direction === 'asc' ? comparison : -comparison;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const memoryAdapter = (
|
|
146
|
+
config: MemoryAdapterConfig = {},
|
|
147
|
+
store = new Map(),
|
|
148
|
+
) => {
|
|
149
|
+
const storeInstance = new MemoryStore(config.initialData, store);
|
|
150
|
+
|
|
151
|
+
const adapterInstance = createAdapterFactory({
|
|
152
|
+
config: {
|
|
153
|
+
adapterId: 'memory-adapter',
|
|
154
|
+
adapterName: 'Memory Adapter',
|
|
155
|
+
usePlural: config.usePlural ?? false,
|
|
156
|
+
debugLogs: config.debugLogs ?? false,
|
|
157
|
+
supportsJSON: true,
|
|
158
|
+
supportsDates: true,
|
|
159
|
+
supportsBooleans: true,
|
|
160
|
+
supportsNumericIds: false,
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
adapter: ({
|
|
164
|
+
debugLog,
|
|
165
|
+
transformInput,
|
|
166
|
+
transformOutput,
|
|
167
|
+
getModelName,
|
|
168
|
+
transformWhereClause,
|
|
169
|
+
}) => ({
|
|
170
|
+
create: async ({ data, model, select }) => {
|
|
171
|
+
debugLog('CREATE', { model, data });
|
|
172
|
+
const modelName = getModelName(model);
|
|
173
|
+
const modelData = storeInstance.getModel(modelName);
|
|
174
|
+
|
|
175
|
+
const transformedData = await transformInput(data, model, 'create');
|
|
176
|
+
|
|
177
|
+
if (!transformedData.id) {
|
|
178
|
+
transformedData.id = crypto.randomUUID();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
modelData.set(transformedData.id, { ...transformedData, ...data });
|
|
182
|
+
|
|
183
|
+
if (data.email_address) {
|
|
184
|
+
modelData.set(transformedData.id, {
|
|
185
|
+
...transformedData,
|
|
186
|
+
email: data.email_address,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const created = modelData.get(transformedData.id);
|
|
190
|
+
|
|
191
|
+
const out = (await transformOutput(created, model, select)) as any;
|
|
192
|
+
|
|
193
|
+
return out;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
findOne: async ({ where, model, select }) => {
|
|
197
|
+
debugLog('FIND_ONE', { model, where });
|
|
198
|
+
const modelName = getModelName(model);
|
|
199
|
+
const modelData = storeInstance.getModel(modelName);
|
|
200
|
+
const transformedWhere = transformWhereClause({ model, where });
|
|
201
|
+
|
|
202
|
+
for (const record of modelData.values()) {
|
|
203
|
+
if (matchesWhere(record, transformedWhere)) {
|
|
204
|
+
const t = (await transformOutput(record, model, select)) as any;
|
|
205
|
+
|
|
206
|
+
return t;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
findMany: async ({ where, model, limit, offset, sortBy }) => {
|
|
213
|
+
debugLog('FIND_MANY', { model, where });
|
|
214
|
+
|
|
215
|
+
const modelName = getModelName(model);
|
|
216
|
+
const modelData = storeInstance.getModel(modelName);
|
|
217
|
+
const transformedWhere = transformWhereClause({ model, where });
|
|
218
|
+
|
|
219
|
+
let results = Array.from(modelData.values()).filter((record) =>
|
|
220
|
+
matchesWhere(record, transformedWhere),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (sortBy) {
|
|
224
|
+
results = applySorting(results, sortBy);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (offset) {
|
|
228
|
+
results = results.slice(offset);
|
|
229
|
+
}
|
|
230
|
+
if (limit) {
|
|
231
|
+
results = results.slice(0, limit);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Promise.all(
|
|
235
|
+
results.map((record) => transformOutput(record, model)),
|
|
236
|
+
) as any;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
update: async ({ where, update, model }) => {
|
|
240
|
+
debugLog('UPDATE', { model, where });
|
|
241
|
+
|
|
242
|
+
const modelName = getModelName(model);
|
|
243
|
+
const modelData = storeInstance.getModel(modelName);
|
|
244
|
+
const transformedWhere = transformWhereClause({ model, where });
|
|
245
|
+
|
|
246
|
+
for (const [id, record] of modelData.entries()) {
|
|
247
|
+
if (matchesWhere(record, transformedWhere)) {
|
|
248
|
+
const transformedData = await transformInput(
|
|
249
|
+
update as any,
|
|
250
|
+
model,
|
|
251
|
+
'update',
|
|
252
|
+
);
|
|
253
|
+
const updated = { ...record, ...transformedData };
|
|
254
|
+
modelData.set(id, updated);
|
|
255
|
+
return transformOutput(updated, model) as any;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
updateMany: async ({ where, update, model }) => {
|
|
262
|
+
debugLog('UPDATE_MANY', { model, where });
|
|
263
|
+
const modelName = getModelName(model);
|
|
264
|
+
const modelData = storeInstance.getModel(modelName);
|
|
265
|
+
const transformedWhere = transformWhereClause({ model, where });
|
|
266
|
+
|
|
267
|
+
let count = 0;
|
|
268
|
+
const transformedData = await transformInput(update, model, 'update');
|
|
269
|
+
|
|
270
|
+
for (const [id, record] of modelData.entries()) {
|
|
271
|
+
if (matchesWhere(record, transformedWhere)) {
|
|
272
|
+
modelData.set(id, { ...record, ...transformedData });
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return count;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
delete: async ({ where, model }) => {
|
|
280
|
+
debugLog('DELETE', { model, where });
|
|
281
|
+
const modelName = getModelName(model);
|
|
282
|
+
const modelData = storeInstance.getModel(modelName);
|
|
283
|
+
|
|
284
|
+
for (const [id, record] of modelData.entries()) {
|
|
285
|
+
if (matchesWhere(record, where)) {
|
|
286
|
+
modelData.delete(id);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
deleteMany: async ({ where, model }) => {
|
|
293
|
+
debugLog('DELETE_MANY', { model, where });
|
|
294
|
+
const modelName = getModelName(model);
|
|
295
|
+
const modelData = storeInstance.getModel(modelName);
|
|
296
|
+
|
|
297
|
+
const toDelete: string[] = [];
|
|
298
|
+
for (const [id, record] of modelData.entries()) {
|
|
299
|
+
if (matchesWhere(record, where)) {
|
|
300
|
+
toDelete.push(id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
toDelete.forEach((id) => modelData.delete(id));
|
|
305
|
+
return toDelete.length;
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
count: async ({ where, model }) => {
|
|
309
|
+
const modelName = getModelName(model);
|
|
310
|
+
const modelData = storeInstance.getModel(modelName);
|
|
311
|
+
|
|
312
|
+
return Array.from(modelData.values()).filter((record) =>
|
|
313
|
+
matchesWhere(record, where),
|
|
314
|
+
).length;
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Add utility methods to the adapter
|
|
320
|
+
return Object.assign(adapterInstance, {
|
|
321
|
+
clear: () => store.clear(),
|
|
322
|
+
getAllData: () => storeInstance.getAllData(),
|
|
323
|
+
getStore: () => store,
|
|
324
|
+
});
|
|
325
|
+
};
|
package/src/kysely.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { TestAPI } from 'vitest';
|
|
|
3
3
|
import { VitestKyselyTransactionIsolator } from './VitestKyselyTransactionIsolator';
|
|
4
4
|
import {
|
|
5
5
|
type DatabaseConnection,
|
|
6
|
+
type FixtureCreators,
|
|
6
7
|
IsolationLevel,
|
|
8
|
+
extendWithFixtures as baseExtendWithFixtures,
|
|
7
9
|
} from './VitestTransactionIsolator';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -15,6 +17,10 @@ export { KyselyFactory } from './KyselyFactory';
|
|
|
15
17
|
export { PostgresKyselyMigrator } from './PostgresKyselyMigrator';
|
|
16
18
|
export { VitestKyselyTransactionIsolator } from './VitestKyselyTransactionIsolator';
|
|
17
19
|
export { IsolationLevel } from './VitestTransactionIsolator';
|
|
20
|
+
export type { FixtureCreators } from './VitestTransactionIsolator';
|
|
21
|
+
|
|
22
|
+
// Re-export faker and FakerFactory for type portability in declaration files
|
|
23
|
+
export { faker, type FakerFactory } from './faker';
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Creates a wrapped Vitest test API with automatic transaction rollback for Kysely.
|
|
@@ -82,3 +88,63 @@ export function wrapVitestKyselyTransaction<Database>(
|
|
|
82
88
|
|
|
83
89
|
return wrapper.wrapVitestWithTransaction(connection, setup, level);
|
|
84
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extends a Kysely transaction-wrapped test with additional fixtures.
|
|
94
|
+
* Each fixture receives the transaction and can create dependencies like factories or repositories.
|
|
95
|
+
*
|
|
96
|
+
* @template Database - The database schema type
|
|
97
|
+
* @template Extended - The type of additional fixtures to provide
|
|
98
|
+
* @param wrappedTest - The base wrapped test from wrapVitestKyselyTransaction
|
|
99
|
+
* @param fixtures - Object mapping fixture names to creator functions
|
|
100
|
+
* @returns An extended test API with both trx and the additional fixtures
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* import { test } from 'vitest';
|
|
105
|
+
* import { wrapVitestKyselyTransaction, extendWithFixtures, KyselyFactory } from '@geekmidas/testkit/kysely';
|
|
106
|
+
*
|
|
107
|
+
* // Define your builders
|
|
108
|
+
* const builders = {
|
|
109
|
+
* user: KyselyFactory.createBuilder<DB, 'users'>('users', ({ faker }) => ({
|
|
110
|
+
* name: faker.person.fullName(),
|
|
111
|
+
* email: faker.internet.email(),
|
|
112
|
+
* })),
|
|
113
|
+
* };
|
|
114
|
+
*
|
|
115
|
+
* // Create base wrapped test
|
|
116
|
+
* const baseTest = wrapVitestKyselyTransaction<DB>(test, db, createTestTables);
|
|
117
|
+
*
|
|
118
|
+
* // Extend with fixtures - each fixture receives the transaction
|
|
119
|
+
* const it = extendWithFixtures<DB, { factory: KyselyFactory<DB, typeof builders, {}> }>(
|
|
120
|
+
* baseTest,
|
|
121
|
+
* {
|
|
122
|
+
* factory: (trx) => new KyselyFactory(builders, {}, trx),
|
|
123
|
+
* }
|
|
124
|
+
* );
|
|
125
|
+
*
|
|
126
|
+
* // Use in tests - both trx and factory are available
|
|
127
|
+
* it('should create user with factory', async ({ trx, factory }) => {
|
|
128
|
+
* const user = await factory.insert('user', { name: 'Test User' });
|
|
129
|
+
* expect(user.id).toBeDefined();
|
|
130
|
+
*
|
|
131
|
+
* // Verify in database
|
|
132
|
+
* const found = await trx
|
|
133
|
+
* .selectFrom('users')
|
|
134
|
+
* .where('id', '=', user.id)
|
|
135
|
+
* .selectAll()
|
|
136
|
+
* .executeTakeFirst();
|
|
137
|
+
* expect(found?.name).toBe('Test User');
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function extendWithFixtures<
|
|
142
|
+
Database,
|
|
143
|
+
Extended extends Record<string, unknown>,
|
|
144
|
+
T extends ReturnType<TestAPI['extend']> = ReturnType<TestAPI['extend']>,
|
|
145
|
+
>(wrappedTest: T, fixtures: FixtureCreators<Transaction<Database>, Extended>) {
|
|
146
|
+
return baseExtendWithFixtures<Transaction<Database>, Extended, T>(
|
|
147
|
+
wrappedTest,
|
|
148
|
+
fixtures,
|
|
149
|
+
);
|
|
150
|
+
}
|
package/src/objection.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { TestAPI } from 'vitest';
|
|
|
3
3
|
import { VitestObjectionTransactionIsolator } from './VitestObjectionTransactionIsolator';
|
|
4
4
|
import {
|
|
5
5
|
type DatabaseConnection,
|
|
6
|
+
type FixtureCreators,
|
|
6
7
|
IsolationLevel,
|
|
8
|
+
extendWithFixtures as baseExtendWithFixtures,
|
|
7
9
|
} from './VitestTransactionIsolator';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -16,6 +18,10 @@ export { ObjectionFactory } from './ObjectionFactory';
|
|
|
16
18
|
export { VitestObjectionTransactionIsolator } from './VitestObjectionTransactionIsolator';
|
|
17
19
|
export { IsolationLevel } from './VitestTransactionIsolator';
|
|
18
20
|
export { PostgresObjectionMigrator } from './PostgresObjectionMigrator';
|
|
21
|
+
export type { FixtureCreators } from './VitestTransactionIsolator';
|
|
22
|
+
|
|
23
|
+
// Re-export faker and FakerFactory for type portability in declaration files
|
|
24
|
+
export { faker, type FakerFactory } from './faker';
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Creates a wrapped Vitest test API with automatic transaction rollback for Objection.js.
|
|
@@ -98,3 +104,58 @@ export function wrapVitestObjectionTransaction(
|
|
|
98
104
|
|
|
99
105
|
return wrapper.wrapVitestWithTransaction(conn, setup, level);
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extends an Objection.js transaction-wrapped test with additional fixtures.
|
|
110
|
+
* Each fixture receives the transaction and can create dependencies like factories or repositories.
|
|
111
|
+
*
|
|
112
|
+
* @template Extended - The type of additional fixtures to provide
|
|
113
|
+
* @param wrappedTest - The base wrapped test from wrapVitestObjectionTransaction
|
|
114
|
+
* @param fixtures - Object mapping fixture names to creator functions
|
|
115
|
+
* @returns An extended test API with both trx and the additional fixtures
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* import { test } from 'vitest';
|
|
120
|
+
* import { wrapVitestObjectionTransaction, extendWithFixtures, ObjectionFactory } from '@geekmidas/testkit/objection';
|
|
121
|
+
* import { User } from './models';
|
|
122
|
+
*
|
|
123
|
+
* // Define your builders
|
|
124
|
+
* const builders = {
|
|
125
|
+
* user: ObjectionFactory.createBuilder(User, ({ faker }) => ({
|
|
126
|
+
* name: faker.person.fullName(),
|
|
127
|
+
* email: faker.internet.email(),
|
|
128
|
+
* })),
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* // Create base wrapped test
|
|
132
|
+
* const baseTest = wrapVitestObjectionTransaction(test, knex, createTestTables);
|
|
133
|
+
*
|
|
134
|
+
* // Extend with fixtures - each fixture receives the transaction
|
|
135
|
+
* const it = extendWithFixtures<{ factory: ObjectionFactory<typeof builders, {}> }>(
|
|
136
|
+
* baseTest,
|
|
137
|
+
* {
|
|
138
|
+
* factory: (trx) => new ObjectionFactory(builders, {}, trx),
|
|
139
|
+
* }
|
|
140
|
+
* );
|
|
141
|
+
*
|
|
142
|
+
* // Use in tests - both trx and factory are available
|
|
143
|
+
* it('should create user with factory', async ({ trx, factory }) => {
|
|
144
|
+
* const user = await factory.insert('user', { name: 'Test User' });
|
|
145
|
+
* expect(user.id).toBeDefined();
|
|
146
|
+
*
|
|
147
|
+
* // Verify in database
|
|
148
|
+
* const found = await User.query(trx).findById(user.id);
|
|
149
|
+
* expect(found?.name).toBe('Test User');
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function extendWithFixtures<
|
|
154
|
+
Extended extends Record<string, unknown>,
|
|
155
|
+
T extends ReturnType<TestAPI['extend']> = ReturnType<TestAPI['extend']>,
|
|
156
|
+
>(wrappedTest: T, fixtures: FixtureCreators<Knex.Transaction, Extended>) {
|
|
157
|
+
return baseExtendWithFixtures<Knex.Transaction, Extended, T>(
|
|
158
|
+
wrappedTest,
|
|
159
|
+
fixtures,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"KyselyFactory-BcYkC0t2.mjs","names":["seedFn: Seed","builders: Builders","seeds: Seeds","db: Kysely<DB> | ControlledTransaction<DB, []>","table: TableName","item?: (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) =>\n | Partial<Insertable<DB[TableName]>>\n | Promise<Partial<Insertable<DB[TableName]>>>","autoInsert?: boolean","attrs: Attrs","factory: Factory","db: Kysely<DB>","faker: FakerFactory","data: Partial<Insertable<DB[TableName]>>","faker","builderName: K","attrs?: Parameters<Builders[K]>[0]","count: number","attrs?: any","promises: Promise<any>[]","seedName: K","attrs?: Parameters<Seeds[K]>[0]"],"sources":["../src/KyselyFactory.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n Insertable,\n Kysely,\n Selectable,\n} from 'kysely';\nimport { Factory, type FactorySeed } from './Factory.ts';\nimport { type FakerFactory, faker } from './faker.ts';\n\n/**\n * Factory implementation for Kysely ORM, providing test data creation utilities.\n * Extends the base Factory class with Kysely-specific database operations.\n *\n * @template DB - The database schema type\n * @template Builders - Record of builder functions for creating entities\n * @template Seeds - Record of seed functions for complex test scenarios\n *\n * @example\n * ```typescript\n * // Define your database schema\n * interface Database {\n * users: UsersTable;\n * posts: PostsTable;\n * }\n *\n * // Create builders\n * const builders = {\n * user: KyselyFactory.createBuilder<Database, 'users'>('users', (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * ...attrs\n * })),\n * post: KyselyFactory.createBuilder<Database, 'posts'>('posts', (attrs) => ({\n * title: 'Test Post',\n * content: 'Test content',\n * ...attrs\n * }))\n * };\n *\n * // Create factory instance\n * const factory = new KyselyFactory(builders, seeds, db);\n *\n * // Use in tests\n * const user = await factory.insert('user', { name: 'John Doe' });\n * ```\n */\nexport class KyselyFactory<\n DB,\n Builders extends Record<string, any>,\n Seeds extends Record<string, any>,\n> extends Factory<Builders, Seeds> {\n /**\n * Creates a typed seed function with proper type inference.\n * Inherits from the base Factory class implementation.\n *\n * @template Seed - The seed function type\n * @param seedFn - The seed function to wrap\n * @returns The same seed function with proper typing\n */\n static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {\n return Factory.createSeed(seedFn);\n }\n\n /**\n * Creates a new KyselyFactory instance.\n *\n * @param builders - Record of builder functions for creating individual entities\n * @param seeds - Record of seed functions for creating complex test scenarios\n * @param db - Kysely database instance or controlled transaction\n */\n constructor(\n private builders: Builders,\n private seeds: Seeds,\n private db: Kysely<DB> | ControlledTransaction<DB, []>,\n ) {\n super();\n }\n\n /**\n * Creates a typed builder function for a specific database table.\n * This is a utility method that helps create builders with proper type inference for Kysely.\n *\n * @template DB - The database schema type\n * @template TableName - The name of the table (must be a key of DB)\n * @template Attrs - The attributes type for the builder (defaults to Partial<Insertable>)\n * @template Factory - The factory instance type\n * @template Result - The result type (defaults to Selectable of the table)\n *\n * @param table - The name of the database table\n * @param item - Optional function to provide default values and transformations\n * @param autoInsert - Whether to automatically insert the record (default: true)\n * @returns A builder function that creates and optionally inserts records\n *\n * @example\n * ```typescript\n * // Create a simple builder with defaults\n * const userBuilder = KyselyFactory.createBuilder<DB, 'users'>('users',\n * (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * createdAt: new Date(),\n * ...attrs\n * })\n * );\n *\n * // Create a builder that doesn't auto-insert (useful for nested inserts)\n * const addressBuilder = KyselyFactory.createBuilder<DB, 'addresses'>('addresses',\n * (attrs) => ({\n * street: '123 Main St',\n * city: 'Anytown',\n * ...attrs\n * }),\n * false // Don't auto-insert\n * );\n * ```\n */\n static createBuilder<\n DB,\n TableName extends keyof DB & string,\n Attrs extends Partial<Insertable<DB[TableName]>> = Partial<\n Insertable<DB[TableName]>\n >,\n Factory = any,\n Result = Selectable<DB[TableName]>,\n >(\n table: TableName,\n item?: (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) =>\n | Partial<Insertable<DB[TableName]>>\n | Promise<Partial<Insertable<DB[TableName]>>>,\n autoInsert?: boolean,\n ): (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) => Promise<Result> {\n return async (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) => {\n // Start with attributes\n let data: Partial<Insertable<DB[TableName]>> = { ...attrs };\n\n // Apply defaults\n if (item) {\n const defaults = await item(attrs, factory, db, faker);\n data = { ...defaults, ...data };\n }\n\n // Handle insertion based on autoInsert flag\n if (autoInsert !== false) {\n // Auto insert is enabled by default\n const result = await db\n .insertInto(table)\n .values(data as Insertable<DB[TableName]>)\n .returningAll()\n .executeTakeFirst();\n\n if (!result) {\n throw new Error(`Failed to insert into ${table}`);\n }\n\n return result as Result;\n } else {\n // Return object for factory to handle insertion\n return { table, data } as any;\n }\n };\n }\n\n /**\n * Inserts a single record into the database using the specified builder.\n * The builder function is responsible for generating the record data with defaults\n * and the factory handles the actual database insertion.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param builderName - The name of the builder to use\n * @param attrs - Optional attributes to override builder defaults\n * @returns A promise resolving to the inserted record\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert with defaults\n * const user = await factory.insert('user');\n *\n * // Insert with overrides\n * const adminUser = await factory.insert('user', {\n * email: 'admin@example.com',\n * role: 'admin'\n * });\n *\n * // Use the inserted record\n * const post = await factory.insert('post', {\n * userId: user.id,\n * title: 'My First Post'\n * });\n * ```\n */\n async insert<K extends keyof Builders>(\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Factory \"${\n builderName as string\n }\" does not exist. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const result = await this.builders[builderName](\n attrs || {},\n this,\n this.db,\n faker,\n );\n\n // For Kysely, we expect the builder to return an object with table and data properties\n // or to handle the insertion itself and return the inserted record\n if (\n result &&\n typeof result === 'object' &&\n 'table' in result &&\n 'data' in result\n ) {\n // If the builder returns {table: string, data: object}, we insert it\n const inserted = await this.db\n .insertInto(result.table)\n .values(result.data)\n .returningAll()\n .executeTakeFirst();\n\n return inserted as any;\n }\n\n // Otherwise, assume the builder handled the insertion itself\n return result;\n }\n\n /**\n * Inserts multiple records into the database using the specified builder.\n * Supports both static attributes and dynamic attribute generation via a function.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param count - The number of records to insert\n * @param builderName - The name of the builder to use\n * @param attrs - Static attributes or a function that generates attributes for each record\n * @returns A promise resolving to an array of inserted records\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert multiple with same attributes\n * const users = await factory.insertMany(5, 'user', { role: 'member' });\n *\n * // Insert multiple with dynamic attributes\n * const posts = await factory.insertMany(10, 'post', (idx, faker) => ({\n * title: `Post ${idx + 1}`,\n * content: faker.lorem.paragraph(),\n * publishedAt: faker.date.past()\n * }));\n *\n * // Create users with sequential emails\n * const admins = await factory.insertMany(3, 'user', (idx) => ({\n * email: `admin${idx + 1}@example.com`,\n * role: 'admin'\n * }));\n * ```\n */\n // Method overloads for better type inference\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs: (\n idx: number,\n faker: FakerFactory,\n ) => Parameters<Builders[K]>[0] | Promise<Parameters<Builders[K]>[0]>,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: any,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Builder \"${\n builderName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const promises: Promise<any>[] = [];\n\n for (let i = 0; i < count; i++) {\n const newAttrs =\n typeof attrs === 'function' ? await attrs(i, faker) : attrs;\n promises.push(this.insert(builderName, newAttrs));\n }\n\n return Promise.all(promises);\n }\n\n /**\n * Executes a seed function to create complex test scenarios with multiple related records.\n * Seeds are useful for setting up complete test environments with realistic data relationships.\n *\n * @template K - The seed name (must be a key of Seeds)\n * @param seedName - The name of the seed to execute\n * @param attrs - Optional configuration attributes for the seed\n * @returns The result of the seed function (typically the primary record created)\n * @throws Error if the specified seed doesn't exist\n *\n * @example\n * ```typescript\n * // Execute a simple seed\n * const user = await factory.seed('userWithProfile');\n *\n * // Execute a seed with configuration\n * const author = await factory.seed('authorWithBooks', {\n * bookCount: 5,\n * includeReviews: true\n * });\n *\n * // Use seed result in tests\n * const company = await factory.seed('companyWithDepartments', {\n * departmentCount: 3,\n * employeesPerDepartment: 10\n * });\n * expect(company.departments).toHaveLength(3);\n * ```\n */\n seed<K extends keyof Seeds>(\n seedName: K,\n attrs?: Parameters<Seeds[K]>[0],\n ): ReturnType<Seeds[K]> {\n if (!(seedName in this.seeds)) {\n throw new Error(\n `Seed \"${\n seedName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n return this.seeds[seedName](attrs || {}, this, this.db);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,gBAAb,cAIU,QAAyB;;;;;;;;;CASjC,OAAO,WAAqCA,QAAoB;AAC9D,SAAO,QAAQ,WAAW,OAAO;CAClC;;;;;;;;CASD,YACUC,UACAC,OACAC,IACR;AACA,SAAO;EAJC;EACA;EACA;CAGT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCD,OAAO,cASLC,OACAC,MAQAC,YAMmB;AACnB,SAAO,OACLC,OACAC,SACAC,IACAC,YACG;GAEH,IAAIC,OAA2C,EAAE,GAAG,MAAO;AAG3D,OAAI,MAAM;IACR,MAAM,WAAW,MAAM,KAAK,OAAO,SAAS,IAAIC,QAAM;AACtD,WAAO;KAAE,GAAG;KAAU,GAAG;IAAM;GAChC;AAGD,OAAI,eAAe,OAAO;IAExB,MAAM,SAAS,MAAM,GAClB,WAAW,MAAM,CACjB,OAAO,KAAkC,CACzC,cAAc,CACd,kBAAkB;AAErB,SAAK,OACH,OAAM,IAAI,OAAO,wBAAwB,MAAM;AAGjD,WAAO;GACR,MAEC,QAAO;IAAE;IAAO;GAAM;EAEzB;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BD,MAAM,OACJC,aACAC,OAC2C;AAC3C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAM,SAAS,MAAM,KAAK,SAAS,aACjC,SAAS,CAAE,GACX,MACA,KAAK,IACL,MACD;AAID,MACE,iBACO,WAAW,YAClB,WAAW,UACX,UAAU,QACV;GAEA,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,OAAO,MAAM,CACxB,OAAO,OAAO,KAAK,CACnB,cAAc,CACd,kBAAkB;AAErB,UAAO;EACR;AAGD,SAAO;CACR;CA8CD,MAAM,WACJC,OACAF,aACAG,OAC6C;AAC7C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAMC,WAA2B,CAAE;AAEnC,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,kBACG,UAAU,aAAa,MAAM,MAAM,GAAG,MAAM,GAAG;AACxD,YAAS,KAAK,KAAK,OAAO,aAAa,SAAS,CAAC;EAClD;AAED,SAAO,QAAQ,IAAI,SAAS;CAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BD,KACEC,UACAC,OACsB;AACtB,QAAM,YAAY,KAAK,OACrB,OAAM,IAAI,OACP,QACC,SACD;AAIL,SAAO,KAAK,MAAM,UAAU,SAAS,CAAE,GAAE,MAAM,KAAK,GAAG;CACxD;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"KyselyFactory-Cf0o2YxO.cjs","names":["Factory","seedFn: Seed","builders: Builders","seeds: Seeds","db: Kysely<DB> | ControlledTransaction<DB, []>","table: TableName","item?: (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) =>\n | Partial<Insertable<DB[TableName]>>\n | Promise<Partial<Insertable<DB[TableName]>>>","autoInsert?: boolean","attrs: Attrs","factory: Factory","db: Kysely<DB>","faker: FakerFactory","data: Partial<Insertable<DB[TableName]>>","faker","builderName: K","attrs?: Parameters<Builders[K]>[0]","count: number","attrs?: any","promises: Promise<any>[]","seedName: K","attrs?: Parameters<Seeds[K]>[0]"],"sources":["../src/KyselyFactory.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n Insertable,\n Kysely,\n Selectable,\n} from 'kysely';\nimport { Factory, type FactorySeed } from './Factory.ts';\nimport { type FakerFactory, faker } from './faker.ts';\n\n/**\n * Factory implementation for Kysely ORM, providing test data creation utilities.\n * Extends the base Factory class with Kysely-specific database operations.\n *\n * @template DB - The database schema type\n * @template Builders - Record of builder functions for creating entities\n * @template Seeds - Record of seed functions for complex test scenarios\n *\n * @example\n * ```typescript\n * // Define your database schema\n * interface Database {\n * users: UsersTable;\n * posts: PostsTable;\n * }\n *\n * // Create builders\n * const builders = {\n * user: KyselyFactory.createBuilder<Database, 'users'>('users', (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * ...attrs\n * })),\n * post: KyselyFactory.createBuilder<Database, 'posts'>('posts', (attrs) => ({\n * title: 'Test Post',\n * content: 'Test content',\n * ...attrs\n * }))\n * };\n *\n * // Create factory instance\n * const factory = new KyselyFactory(builders, seeds, db);\n *\n * // Use in tests\n * const user = await factory.insert('user', { name: 'John Doe' });\n * ```\n */\nexport class KyselyFactory<\n DB,\n Builders extends Record<string, any>,\n Seeds extends Record<string, any>,\n> extends Factory<Builders, Seeds> {\n /**\n * Creates a typed seed function with proper type inference.\n * Inherits from the base Factory class implementation.\n *\n * @template Seed - The seed function type\n * @param seedFn - The seed function to wrap\n * @returns The same seed function with proper typing\n */\n static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {\n return Factory.createSeed(seedFn);\n }\n\n /**\n * Creates a new KyselyFactory instance.\n *\n * @param builders - Record of builder functions for creating individual entities\n * @param seeds - Record of seed functions for creating complex test scenarios\n * @param db - Kysely database instance or controlled transaction\n */\n constructor(\n private builders: Builders,\n private seeds: Seeds,\n private db: Kysely<DB> | ControlledTransaction<DB, []>,\n ) {\n super();\n }\n\n /**\n * Creates a typed builder function for a specific database table.\n * This is a utility method that helps create builders with proper type inference for Kysely.\n *\n * @template DB - The database schema type\n * @template TableName - The name of the table (must be a key of DB)\n * @template Attrs - The attributes type for the builder (defaults to Partial<Insertable>)\n * @template Factory - The factory instance type\n * @template Result - The result type (defaults to Selectable of the table)\n *\n * @param table - The name of the database table\n * @param item - Optional function to provide default values and transformations\n * @param autoInsert - Whether to automatically insert the record (default: true)\n * @returns A builder function that creates and optionally inserts records\n *\n * @example\n * ```typescript\n * // Create a simple builder with defaults\n * const userBuilder = KyselyFactory.createBuilder<DB, 'users'>('users',\n * (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * createdAt: new Date(),\n * ...attrs\n * })\n * );\n *\n * // Create a builder that doesn't auto-insert (useful for nested inserts)\n * const addressBuilder = KyselyFactory.createBuilder<DB, 'addresses'>('addresses',\n * (attrs) => ({\n * street: '123 Main St',\n * city: 'Anytown',\n * ...attrs\n * }),\n * false // Don't auto-insert\n * );\n * ```\n */\n static createBuilder<\n DB,\n TableName extends keyof DB & string,\n Attrs extends Partial<Insertable<DB[TableName]>> = Partial<\n Insertable<DB[TableName]>\n >,\n Factory = any,\n Result = Selectable<DB[TableName]>,\n >(\n table: TableName,\n item?: (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) =>\n | Partial<Insertable<DB[TableName]>>\n | Promise<Partial<Insertable<DB[TableName]>>>,\n autoInsert?: boolean,\n ): (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) => Promise<Result> {\n return async (\n attrs: Attrs,\n factory: Factory,\n db: Kysely<DB>,\n faker: FakerFactory,\n ) => {\n // Start with attributes\n let data: Partial<Insertable<DB[TableName]>> = { ...attrs };\n\n // Apply defaults\n if (item) {\n const defaults = await item(attrs, factory, db, faker);\n data = { ...defaults, ...data };\n }\n\n // Handle insertion based on autoInsert flag\n if (autoInsert !== false) {\n // Auto insert is enabled by default\n const result = await db\n .insertInto(table)\n .values(data as Insertable<DB[TableName]>)\n .returningAll()\n .executeTakeFirst();\n\n if (!result) {\n throw new Error(`Failed to insert into ${table}`);\n }\n\n return result as Result;\n } else {\n // Return object for factory to handle insertion\n return { table, data } as any;\n }\n };\n }\n\n /**\n * Inserts a single record into the database using the specified builder.\n * The builder function is responsible for generating the record data with defaults\n * and the factory handles the actual database insertion.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param builderName - The name of the builder to use\n * @param attrs - Optional attributes to override builder defaults\n * @returns A promise resolving to the inserted record\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert with defaults\n * const user = await factory.insert('user');\n *\n * // Insert with overrides\n * const adminUser = await factory.insert('user', {\n * email: 'admin@example.com',\n * role: 'admin'\n * });\n *\n * // Use the inserted record\n * const post = await factory.insert('post', {\n * userId: user.id,\n * title: 'My First Post'\n * });\n * ```\n */\n async insert<K extends keyof Builders>(\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Factory \"${\n builderName as string\n }\" does not exist. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const result = await this.builders[builderName](\n attrs || {},\n this,\n this.db,\n faker,\n );\n\n // For Kysely, we expect the builder to return an object with table and data properties\n // or to handle the insertion itself and return the inserted record\n if (\n result &&\n typeof result === 'object' &&\n 'table' in result &&\n 'data' in result\n ) {\n // If the builder returns {table: string, data: object}, we insert it\n const inserted = await this.db\n .insertInto(result.table)\n .values(result.data)\n .returningAll()\n .executeTakeFirst();\n\n return inserted as any;\n }\n\n // Otherwise, assume the builder handled the insertion itself\n return result;\n }\n\n /**\n * Inserts multiple records into the database using the specified builder.\n * Supports both static attributes and dynamic attribute generation via a function.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param count - The number of records to insert\n * @param builderName - The name of the builder to use\n * @param attrs - Static attributes or a function that generates attributes for each record\n * @returns A promise resolving to an array of inserted records\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert multiple with same attributes\n * const users = await factory.insertMany(5, 'user', { role: 'member' });\n *\n * // Insert multiple with dynamic attributes\n * const posts = await factory.insertMany(10, 'post', (idx, faker) => ({\n * title: `Post ${idx + 1}`,\n * content: faker.lorem.paragraph(),\n * publishedAt: faker.date.past()\n * }));\n *\n * // Create users with sequential emails\n * const admins = await factory.insertMany(3, 'user', (idx) => ({\n * email: `admin${idx + 1}@example.com`,\n * role: 'admin'\n * }));\n * ```\n */\n // Method overloads for better type inference\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs: (\n idx: number,\n faker: FakerFactory,\n ) => Parameters<Builders[K]>[0] | Promise<Parameters<Builders[K]>[0]>,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: any,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Builder \"${\n builderName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const promises: Promise<any>[] = [];\n\n for (let i = 0; i < count; i++) {\n const newAttrs =\n typeof attrs === 'function' ? await attrs(i, faker) : attrs;\n promises.push(this.insert(builderName, newAttrs));\n }\n\n return Promise.all(promises);\n }\n\n /**\n * Executes a seed function to create complex test scenarios with multiple related records.\n * Seeds are useful for setting up complete test environments with realistic data relationships.\n *\n * @template K - The seed name (must be a key of Seeds)\n * @param seedName - The name of the seed to execute\n * @param attrs - Optional configuration attributes for the seed\n * @returns The result of the seed function (typically the primary record created)\n * @throws Error if the specified seed doesn't exist\n *\n * @example\n * ```typescript\n * // Execute a simple seed\n * const user = await factory.seed('userWithProfile');\n *\n * // Execute a seed with configuration\n * const author = await factory.seed('authorWithBooks', {\n * bookCount: 5,\n * includeReviews: true\n * });\n *\n * // Use seed result in tests\n * const company = await factory.seed('companyWithDepartments', {\n * departmentCount: 3,\n * employeesPerDepartment: 10\n * });\n * expect(company.departments).toHaveLength(3);\n * ```\n */\n seed<K extends keyof Seeds>(\n seedName: K,\n attrs?: Parameters<Seeds[K]>[0],\n ): ReturnType<Seeds[K]> {\n if (!(seedName in this.seeds)) {\n throw new Error(\n `Seed \"${\n seedName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n return this.seeds[seedName](attrs || {}, this, this.db);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,gBAAb,cAIUA,wBAAyB;;;;;;;;;CASjC,OAAO,WAAqCC,QAAoB;AAC9D,SAAO,wBAAQ,WAAW,OAAO;CAClC;;;;;;;;CASD,YACUC,UACAC,OACAC,IACR;AACA,SAAO;EAJC;EACA;EACA;CAGT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCD,OAAO,cASLC,OACAC,MAQAC,YAMmB;AACnB,SAAO,OACLC,OACAC,SACAC,IACAC,YACG;GAEH,IAAIC,OAA2C,EAAE,GAAG,MAAO;AAG3D,OAAI,MAAM;IACR,MAAM,WAAW,MAAM,KAAK,OAAO,SAAS,IAAIC,QAAM;AACtD,WAAO;KAAE,GAAG;KAAU,GAAG;IAAM;GAChC;AAGD,OAAI,eAAe,OAAO;IAExB,MAAM,SAAS,MAAM,GAClB,WAAW,MAAM,CACjB,OAAO,KAAkC,CACzC,cAAc,CACd,kBAAkB;AAErB,SAAK,OACH,OAAM,IAAI,OAAO,wBAAwB,MAAM;AAGjD,WAAO;GACR,MAEC,QAAO;IAAE;IAAO;GAAM;EAEzB;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BD,MAAM,OACJC,aACAC,OAC2C;AAC3C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAM,SAAS,MAAM,KAAK,SAAS,aACjC,SAAS,CAAE,GACX,MACA,KAAK,IACLF,oBACD;AAID,MACE,iBACO,WAAW,YAClB,WAAW,UACX,UAAU,QACV;GAEA,MAAM,WAAW,MAAM,KAAK,GACzB,WAAW,OAAO,MAAM,CACxB,OAAO,OAAO,KAAK,CACnB,cAAc,CACd,kBAAkB;AAErB,UAAO;EACR;AAGD,SAAO;CACR;CA8CD,MAAM,WACJG,OACAF,aACAG,OAC6C;AAC7C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAMC,WAA2B,CAAE;AAEnC,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,kBACG,UAAU,aAAa,MAAM,MAAM,GAAGL,oBAAM,GAAG;AACxD,YAAS,KAAK,KAAK,OAAO,aAAa,SAAS,CAAC;EAClD;AAED,SAAO,QAAQ,IAAI,SAAS;CAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BD,KACEM,UACAC,OACsB;AACtB,QAAM,YAAY,KAAK,OACrB,OAAM,IAAI,OACP,QACC,SACD;AAIL,SAAO,KAAK,MAAM,UAAU,SAAS,CAAE,GAAE,MAAM,KAAK,GAAG;CACxD;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectionFactory-8hebmnai.mjs","names":["seedFn: Seed","ModelClass: TModel","item?: (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Partial<InstanceType<TModel>> | Promise<Partial<InstanceType<TModel>>>","autoInsert?: boolean","attrs: Attrs","factory: Factory","db: Knex","faker: FakerFactory","data: Partial<InstanceType<TModel>>","faker","builders: Builders","seeds: Seeds","builderName: K","attrs?: Parameters<Builders[K]>[0]","count: number","attrs?: any","records: any[]","record: any","seedName: K","attrs?: Parameters<Seeds[K]>[0]"],"sources":["../src/ObjectionFactory.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport type { Model } from 'objection';\nimport { Factory, type FactorySeed } from './Factory.ts';\nimport { type FakerFactory, faker } from './faker.ts';\n\n/**\n * Factory implementation for Objection.js ORM, providing test data creation utilities.\n * Extends the base Factory class with Objection.js-specific database operations.\n *\n * @template Builders - Record of builder functions for creating entities\n * @template Seeds - Record of seed functions for complex test scenarios\n *\n * @example\n * ```typescript\n * // Define your models with Objection.js\n * class User extends Model {\n * static tableName = 'users';\n * }\n *\n * // Create builders\n * const builders = {\n * user: (attrs) => User.fromJson({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * ...attrs\n * }),\n * post: (attrs) => Post.fromJson({\n * title: 'Test Post',\n * content: 'Test content',\n * ...attrs\n * })\n * };\n *\n * // Create factory instance\n * const factory = new ObjectionFactory(builders, seeds, knex);\n *\n * // Use in tests\n * const user = await factory.insert('user', { name: 'John Doe' });\n * ```\n */\nexport class ObjectionFactory<\n Builders extends Record<string, any>,\n Seeds extends Record<string, any>,\n> extends Factory<Builders, Seeds> {\n /**\n * Creates a typed seed function with proper type inference.\n * Inherits from the base Factory class implementation.\n *\n * @template Seed - The seed function type\n * @param seedFn - The seed function to wrap\n * @returns The same seed function with proper typing\n */\n static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {\n return Factory.createSeed(seedFn);\n }\n\n /**\n * Creates a typed builder function for Objection.js models.\n * This is a utility method that helps create builders with proper type inference.\n *\n * @template TModel - The Objection.js Model class type\n * @template Attrs - The attributes type for the builder (defaults to Partial of model)\n * @template Factory - The factory instance type\n * @template Result - The result type (defaults to the model instance)\n *\n * @param ModelClass - The Objection.js Model class\n * @param item - Optional function to provide default values and transformations\n * @param autoInsert - Whether to automatically insert the record (default: true)\n * @returns A builder function that creates and optionally inserts records\n *\n * @example\n * ```typescript\n * // Create a simple builder with defaults\n * const userBuilder = ObjectionFactory.createBuilder(User,\n * (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * createdAt: new Date(),\n * ...attrs\n * })\n * );\n *\n * // Create a builder that doesn't auto-insert (useful for nested inserts)\n * const addressBuilder = ObjectionFactory.createBuilder(Address,\n * (attrs) => ({\n * street: '123 Main St',\n * city: 'Anytown',\n * ...attrs\n * }),\n * false // Don't auto-insert\n * );\n *\n * // Use with relations\n * const postBuilder = ObjectionFactory.createBuilder(Post,\n * async (attrs, factory) => ({\n * title: faker.lorem.sentence(),\n * content: faker.lorem.paragraphs(),\n * authorId: attrs.authorId || (await factory.insert('user')).id,\n * ...attrs\n * })\n * );\n * ```\n */\n static createBuilder<\n TModel extends typeof Model,\n Attrs extends Partial<InstanceType<TModel>> = Partial<InstanceType<TModel>>,\n Factory = any,\n Result = InstanceType<TModel>,\n >(\n ModelClass: TModel,\n item?: (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Partial<InstanceType<TModel>> | Promise<Partial<InstanceType<TModel>>>,\n autoInsert?: boolean,\n ): (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Promise<Result> {\n return async (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => {\n // Start with attributes\n let data: Partial<InstanceType<TModel>> = { ...attrs };\n\n // Apply defaults\n if (item) {\n const defaults = await item(attrs, factory, db, faker);\n data = { ...defaults, ...data };\n }\n\n // Create model instance\n const model = ModelClass.fromJson(data) as InstanceType<TModel>;\n\n // Handle insertion based on autoInsert flag\n if (autoInsert !== false) {\n // Auto insert is enabled by default\n // Extract only defined values for insertion\n const insertData = Object.entries(model).reduce((acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n }, {} as any);\n\n // Use static query method to insert data directly\n // @ts-ignore\n const result = await ModelClass.query(db).insert(insertData);\n return result as Result;\n } else {\n // Return model for factory to handle insertion\n return model as Result;\n }\n };\n }\n\n /**\n * Creates a new ObjectionFactory instance.\n *\n * @param builders - Record of builder functions for creating individual entities\n * @param seeds - Record of seed functions for creating complex test scenarios\n * @param db - Knex database connection instance\n */\n constructor(\n private builders: Builders,\n private seeds: Seeds,\n private db: Knex,\n ) {\n super();\n }\n\n /**\n * Inserts a single record into the database using the specified builder.\n * Uses Objection.js's insertGraph method to handle nested relations.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param builderName - The name of the builder to use\n * @param attrs - Optional attributes to override builder defaults\n * @returns A promise resolving to the inserted record with all relations\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert with defaults\n * const user = await factory.insert('user');\n *\n * // Insert with overrides\n * const adminUser = await factory.insert('user', {\n * email: 'admin@example.com',\n * role: 'admin'\n * });\n *\n * // Insert with nested relations\n * const userWithProfile = await factory.insert('user', {\n * name: 'John Doe',\n * profile: {\n * bio: 'Software Developer',\n * avatar: 'avatar.jpg'\n * }\n * });\n * ```\n */\n async insert<K extends keyof Builders>(\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Factory \"${\n builderName as string\n }\" does not exist. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const result = await this.builders[builderName](\n attrs || {},\n this,\n this.db,\n faker,\n );\n\n // If the builder returns a model instance, insert it\n if (result && typeof result.$query === 'function') {\n // Extract data from model, excluding undefined values and id\n const insertData = Object.entries(result).reduce((acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n }, {} as any);\n\n // Use the model's constructor to get the query builder\n return await result.constructor.query(this.db).insert(insertData);\n }\n\n // Otherwise, assume the builder handled insertion itself\n return result;\n }\n /**\n * Inserts multiple records into the database using the specified builder.\n * Supports both static attributes and dynamic attribute generation via a function.\n *\n * @param count - The number of records to insert\n * @param builderName - The name of the builder to use\n * @param attrs - Static attributes or a function that generates attributes for each record\n * @returns A promise resolving to an array of inserted records\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert multiple with same attributes\n * const users = await factory.insertMany(5, 'user', { role: 'member' });\n *\n * // Insert multiple with dynamic attributes\n * const posts = await factory.insertMany(10, 'post', (idx) => ({\n * title: `Post ${idx + 1}`,\n * content: `Content for post ${idx + 1}`,\n * publishedAt: new Date()\n * }));\n *\n * // Create users with sequential emails\n * const admins = await factory.insertMany(3, 'user', (idx) => ({\n * email: `admin${idx + 1}@example.com`,\n * role: 'admin'\n * }));\n * ```\n */\n // Method overloads for better type inference\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs: (idx: number, faker: FakerFactory) => Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: any,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Builder \"${\n builderName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const records: any[] = [];\n for (let i = 0; i < count; i++) {\n const newAttrs =\n typeof attrs === 'function' ? await (attrs as any)(i, faker) : attrs;\n\n records.push(\n this.builders[builderName](newAttrs, this, this.db, faker).then(\n (record: any) => {\n // If the builder returns a model instance, insert it\n if (record && typeof record.$query === 'function') {\n // Extract data from model, excluding undefined values and id\n const insertData = Object.entries(record).reduce(\n (acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n },\n {} as any,\n );\n\n // Use the model's constructor to get the query builder\n return record.constructor.query(this.db).insert(insertData);\n }\n // Otherwise, assume the builder handled insertion itself\n return record;\n },\n ),\n );\n }\n\n return Promise.all(records);\n }\n /**\n * Executes a seed function to create complex test scenarios with multiple related records.\n * Seeds are useful for setting up complete test environments with realistic data relationships.\n *\n * @template K - The seed name (must be a key of Seeds)\n * @param seedName - The name of the seed to execute\n * @param attrs - Optional configuration attributes for the seed\n * @returns The result of the seed function (typically the primary record created)\n * @throws Error if the specified seed doesn't exist\n *\n * @example\n * ```typescript\n * // Execute a simple seed\n * const user = await factory.seed('userWithProfile');\n *\n * // Execute a seed with configuration\n * const author = await factory.seed('authorWithBooks', {\n * bookCount: 5,\n * includeReviews: true\n * });\n *\n * // Use seed result in tests with Objection.js relations\n * const company = await factory.seed('companyWithDepartments', {\n * departmentCount: 3,\n * employeesPerDepartment: 10\n * });\n *\n * // Access eager loaded relations\n * const companyWithRelations = await Company.query()\n * .findById(company.id)\n * .withGraphFetched('[departments.employees]');\n * ```\n */\n seed<K extends keyof Seeds>(\n seedName: K,\n attrs?: Parameters<Seeds[K]>[0],\n ): ReturnType<Seeds[K]> {\n if (!(seedName in this.seeds)) {\n throw new Error(\n `Seed \"${\n seedName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n return this.seeds[seedName](attrs || {}, this, this.db);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,mBAAb,cAGU,QAAyB;;;;;;;;;CASjC,OAAO,WAAqCA,QAAoB;AAC9D,SAAO,QAAQ,WAAW,OAAO;CAClC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDD,OAAO,cAMLC,YACAC,MAMAC,YAMmB;AACnB,SAAO,OACLC,OACAC,SACAC,IACAC,YACG;GAEH,IAAIC,OAAsC,EAAE,GAAG,MAAO;AAGtD,OAAI,MAAM;IACR,MAAM,WAAW,MAAM,KAAK,OAAO,SAAS,IAAIC,QAAM;AACtD,WAAO;KAAE,GAAG;KAAU,GAAG;IAAM;GAChC;GAGD,MAAM,QAAQ,WAAW,SAAS,KAAK;AAGvC,OAAI,eAAe,OAAO;IAGxB,MAAM,aAAa,OAAO,QAAQ,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACrE,SAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,YAAO;IACR,GAAE,CAAE,EAAQ;IAIb,MAAM,SAAS,MAAM,WAAW,MAAM,GAAG,CAAC,OAAO,WAAW;AAC5D,WAAO;GACR,MAEC,QAAO;EAEV;CACF;;;;;;;;CASD,YACUC,UACAC,OACAL,IACR;AACA,SAAO;EAJC;EACA;EACA;CAGT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCD,MAAM,OACJM,aACAC,OAC2C;AAC3C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAM,SAAS,MAAM,KAAK,SAAS,aACjC,SAAS,CAAE,GACX,MACA,KAAK,IACL,MACD;AAGD,MAAI,iBAAiB,OAAO,WAAW,YAAY;GAEjD,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACtE,QAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,WAAO;GACR,GAAE,CAAE,EAAQ;AAGb,UAAO,MAAM,OAAO,YAAY,MAAM,KAAK,GAAG,CAAC,OAAO,WAAW;EAClE;AAGD,SAAO;CACR;CAyCD,MAAM,WACJC,OACAF,aACAG,OAC6C;AAC7C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAMC,UAAiB,CAAE;AACzB,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,kBACG,UAAU,aAAa,MAAM,AAAC,MAAc,GAAG,MAAM,GAAG;AAEjE,WAAQ,KACN,KAAK,SAAS,aAAa,UAAU,MAAM,KAAK,IAAI,MAAM,CAAC,KACzD,CAACC,WAAgB;AAEf,QAAI,iBAAiB,OAAO,WAAW,YAAY;KAEjD,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC,OACxC,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACrB,UAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,aAAO;KACR,GACD,CAAE,EACH;AAGD,YAAO,OAAO,YAAY,MAAM,KAAK,GAAG,CAAC,OAAO,WAAW;IAC5D;AAED,WAAO;GACR,EACF,CACF;EACF;AAED,SAAO,QAAQ,IAAI,QAAQ;CAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCD,KACEC,UACAC,OACsB;AACtB,QAAM,YAAY,KAAK,OACrB,OAAM,IAAI,OACP,QACC,SACD;AAIL,SAAO,KAAK,MAAM,UAAU,SAAS,CAAE,GAAE,MAAM,KAAK,GAAG;CACxD;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectionFactory-CDriunkS.cjs","names":["Factory","seedFn: Seed","ModelClass: TModel","item?: (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Partial<InstanceType<TModel>> | Promise<Partial<InstanceType<TModel>>>","autoInsert?: boolean","attrs: Attrs","factory: Factory","db: Knex","faker: FakerFactory","data: Partial<InstanceType<TModel>>","faker","builders: Builders","seeds: Seeds","builderName: K","attrs?: Parameters<Builders[K]>[0]","count: number","attrs?: any","records: any[]","record: any","seedName: K","attrs?: Parameters<Seeds[K]>[0]"],"sources":["../src/ObjectionFactory.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport type { Model } from 'objection';\nimport { Factory, type FactorySeed } from './Factory.ts';\nimport { type FakerFactory, faker } from './faker.ts';\n\n/**\n * Factory implementation for Objection.js ORM, providing test data creation utilities.\n * Extends the base Factory class with Objection.js-specific database operations.\n *\n * @template Builders - Record of builder functions for creating entities\n * @template Seeds - Record of seed functions for complex test scenarios\n *\n * @example\n * ```typescript\n * // Define your models with Objection.js\n * class User extends Model {\n * static tableName = 'users';\n * }\n *\n * // Create builders\n * const builders = {\n * user: (attrs) => User.fromJson({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * ...attrs\n * }),\n * post: (attrs) => Post.fromJson({\n * title: 'Test Post',\n * content: 'Test content',\n * ...attrs\n * })\n * };\n *\n * // Create factory instance\n * const factory = new ObjectionFactory(builders, seeds, knex);\n *\n * // Use in tests\n * const user = await factory.insert('user', { name: 'John Doe' });\n * ```\n */\nexport class ObjectionFactory<\n Builders extends Record<string, any>,\n Seeds extends Record<string, any>,\n> extends Factory<Builders, Seeds> {\n /**\n * Creates a typed seed function with proper type inference.\n * Inherits from the base Factory class implementation.\n *\n * @template Seed - The seed function type\n * @param seedFn - The seed function to wrap\n * @returns The same seed function with proper typing\n */\n static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {\n return Factory.createSeed(seedFn);\n }\n\n /**\n * Creates a typed builder function for Objection.js models.\n * This is a utility method that helps create builders with proper type inference.\n *\n * @template TModel - The Objection.js Model class type\n * @template Attrs - The attributes type for the builder (defaults to Partial of model)\n * @template Factory - The factory instance type\n * @template Result - The result type (defaults to the model instance)\n *\n * @param ModelClass - The Objection.js Model class\n * @param item - Optional function to provide default values and transformations\n * @param autoInsert - Whether to automatically insert the record (default: true)\n * @returns A builder function that creates and optionally inserts records\n *\n * @example\n * ```typescript\n * // Create a simple builder with defaults\n * const userBuilder = ObjectionFactory.createBuilder(User,\n * (attrs, factory, db, faker) => ({\n * id: faker.string.uuid(),\n * name: faker.person.fullName(),\n * email: faker.internet.email(),\n * createdAt: new Date(),\n * ...attrs\n * })\n * );\n *\n * // Create a builder that doesn't auto-insert (useful for nested inserts)\n * const addressBuilder = ObjectionFactory.createBuilder(Address,\n * (attrs) => ({\n * street: '123 Main St',\n * city: 'Anytown',\n * ...attrs\n * }),\n * false // Don't auto-insert\n * );\n *\n * // Use with relations\n * const postBuilder = ObjectionFactory.createBuilder(Post,\n * async (attrs, factory) => ({\n * title: faker.lorem.sentence(),\n * content: faker.lorem.paragraphs(),\n * authorId: attrs.authorId || (await factory.insert('user')).id,\n * ...attrs\n * })\n * );\n * ```\n */\n static createBuilder<\n TModel extends typeof Model,\n Attrs extends Partial<InstanceType<TModel>> = Partial<InstanceType<TModel>>,\n Factory = any,\n Result = InstanceType<TModel>,\n >(\n ModelClass: TModel,\n item?: (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Partial<InstanceType<TModel>> | Promise<Partial<InstanceType<TModel>>>,\n autoInsert?: boolean,\n ): (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => Promise<Result> {\n return async (\n attrs: Attrs,\n factory: Factory,\n db: Knex,\n faker: FakerFactory,\n ) => {\n // Start with attributes\n let data: Partial<InstanceType<TModel>> = { ...attrs };\n\n // Apply defaults\n if (item) {\n const defaults = await item(attrs, factory, db, faker);\n data = { ...defaults, ...data };\n }\n\n // Create model instance\n const model = ModelClass.fromJson(data) as InstanceType<TModel>;\n\n // Handle insertion based on autoInsert flag\n if (autoInsert !== false) {\n // Auto insert is enabled by default\n // Extract only defined values for insertion\n const insertData = Object.entries(model).reduce((acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n }, {} as any);\n\n // Use static query method to insert data directly\n // @ts-ignore\n const result = await ModelClass.query(db).insert(insertData);\n return result as Result;\n } else {\n // Return model for factory to handle insertion\n return model as Result;\n }\n };\n }\n\n /**\n * Creates a new ObjectionFactory instance.\n *\n * @param builders - Record of builder functions for creating individual entities\n * @param seeds - Record of seed functions for creating complex test scenarios\n * @param db - Knex database connection instance\n */\n constructor(\n private builders: Builders,\n private seeds: Seeds,\n private db: Knex,\n ) {\n super();\n }\n\n /**\n * Inserts a single record into the database using the specified builder.\n * Uses Objection.js's insertGraph method to handle nested relations.\n *\n * @template K - The builder name (must be a key of Builders)\n * @param builderName - The name of the builder to use\n * @param attrs - Optional attributes to override builder defaults\n * @returns A promise resolving to the inserted record with all relations\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert with defaults\n * const user = await factory.insert('user');\n *\n * // Insert with overrides\n * const adminUser = await factory.insert('user', {\n * email: 'admin@example.com',\n * role: 'admin'\n * });\n *\n * // Insert with nested relations\n * const userWithProfile = await factory.insert('user', {\n * name: 'John Doe',\n * profile: {\n * bio: 'Software Developer',\n * avatar: 'avatar.jpg'\n * }\n * });\n * ```\n */\n async insert<K extends keyof Builders>(\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Factory \"${\n builderName as string\n }\" does not exist. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const result = await this.builders[builderName](\n attrs || {},\n this,\n this.db,\n faker,\n );\n\n // If the builder returns a model instance, insert it\n if (result && typeof result.$query === 'function') {\n // Extract data from model, excluding undefined values and id\n const insertData = Object.entries(result).reduce((acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n }, {} as any);\n\n // Use the model's constructor to get the query builder\n return await result.constructor.query(this.db).insert(insertData);\n }\n\n // Otherwise, assume the builder handled insertion itself\n return result;\n }\n /**\n * Inserts multiple records into the database using the specified builder.\n * Supports both static attributes and dynamic attribute generation via a function.\n *\n * @param count - The number of records to insert\n * @param builderName - The name of the builder to use\n * @param attrs - Static attributes or a function that generates attributes for each record\n * @returns A promise resolving to an array of inserted records\n * @throws Error if the specified builder doesn't exist\n *\n * @example\n * ```typescript\n * // Insert multiple with same attributes\n * const users = await factory.insertMany(5, 'user', { role: 'member' });\n *\n * // Insert multiple with dynamic attributes\n * const posts = await factory.insertMany(10, 'post', (idx) => ({\n * title: `Post ${idx + 1}`,\n * content: `Content for post ${idx + 1}`,\n * publishedAt: new Date()\n * }));\n *\n * // Create users with sequential emails\n * const admins = await factory.insertMany(3, 'user', (idx) => ({\n * email: `admin${idx + 1}@example.com`,\n * role: 'admin'\n * }));\n * ```\n */\n // Method overloads for better type inference\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs: (idx: number, faker: FakerFactory) => Parameters<Builders[K]>[0],\n ): Promise<Awaited<ReturnType<Builders[K]>>[]>;\n async insertMany<K extends keyof Builders>(\n count: number,\n builderName: K,\n attrs?: any,\n ): Promise<Awaited<ReturnType<Builders[K]>>[]> {\n if (!(builderName in this.builders)) {\n throw new Error(\n `Builder \"${\n builderName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n const records: any[] = [];\n for (let i = 0; i < count; i++) {\n const newAttrs =\n typeof attrs === 'function' ? await (attrs as any)(i, faker) : attrs;\n\n records.push(\n this.builders[builderName](newAttrs, this, this.db, faker).then(\n (record: any) => {\n // If the builder returns a model instance, insert it\n if (record && typeof record.$query === 'function') {\n // Extract data from model, excluding undefined values and id\n const insertData = Object.entries(record).reduce(\n (acc, [key, value]) => {\n if (value !== undefined && key !== 'id') {\n acc[key] = value;\n }\n return acc;\n },\n {} as any,\n );\n\n // Use the model's constructor to get the query builder\n return record.constructor.query(this.db).insert(insertData);\n }\n // Otherwise, assume the builder handled insertion itself\n return record;\n },\n ),\n );\n }\n\n return Promise.all(records);\n }\n /**\n * Executes a seed function to create complex test scenarios with multiple related records.\n * Seeds are useful for setting up complete test environments with realistic data relationships.\n *\n * @template K - The seed name (must be a key of Seeds)\n * @param seedName - The name of the seed to execute\n * @param attrs - Optional configuration attributes for the seed\n * @returns The result of the seed function (typically the primary record created)\n * @throws Error if the specified seed doesn't exist\n *\n * @example\n * ```typescript\n * // Execute a simple seed\n * const user = await factory.seed('userWithProfile');\n *\n * // Execute a seed with configuration\n * const author = await factory.seed('authorWithBooks', {\n * bookCount: 5,\n * includeReviews: true\n * });\n *\n * // Use seed result in tests with Objection.js relations\n * const company = await factory.seed('companyWithDepartments', {\n * departmentCount: 3,\n * employeesPerDepartment: 10\n * });\n *\n * // Access eager loaded relations\n * const companyWithRelations = await Company.query()\n * .findById(company.id)\n * .withGraphFetched('[departments.employees]');\n * ```\n */\n seed<K extends keyof Seeds>(\n seedName: K,\n attrs?: Parameters<Seeds[K]>[0],\n ): ReturnType<Seeds[K]> {\n if (!(seedName in this.seeds)) {\n throw new Error(\n `Seed \"${\n seedName as string\n }\" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,\n );\n }\n\n return this.seeds[seedName](attrs || {}, this, this.db);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,mBAAb,cAGUA,wBAAyB;;;;;;;;;CASjC,OAAO,WAAqCC,QAAoB;AAC9D,SAAO,wBAAQ,WAAW,OAAO;CAClC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDD,OAAO,cAMLC,YACAC,MAMAC,YAMmB;AACnB,SAAO,OACLC,OACAC,SACAC,IACAC,YACG;GAEH,IAAIC,OAAsC,EAAE,GAAG,MAAO;AAGtD,OAAI,MAAM;IACR,MAAM,WAAW,MAAM,KAAK,OAAO,SAAS,IAAIC,QAAM;AACtD,WAAO;KAAE,GAAG;KAAU,GAAG;IAAM;GAChC;GAGD,MAAM,QAAQ,WAAW,SAAS,KAAK;AAGvC,OAAI,eAAe,OAAO;IAGxB,MAAM,aAAa,OAAO,QAAQ,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACrE,SAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,YAAO;IACR,GAAE,CAAE,EAAQ;IAIb,MAAM,SAAS,MAAM,WAAW,MAAM,GAAG,CAAC,OAAO,WAAW;AAC5D,WAAO;GACR,MAEC,QAAO;EAEV;CACF;;;;;;;;CASD,YACUC,UACAC,OACAL,IACR;AACA,SAAO;EAJC;EACA;EACA;CAGT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCD,MAAM,OACJM,aACAC,OAC2C;AAC3C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAM,SAAS,MAAM,KAAK,SAAS,aACjC,SAAS,CAAE,GACX,MACA,KAAK,IACLJ,oBACD;AAGD,MAAI,iBAAiB,OAAO,WAAW,YAAY;GAEjD,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACtE,QAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,WAAO;GACR,GAAE,CAAE,EAAQ;AAGb,UAAO,MAAM,OAAO,YAAY,MAAM,KAAK,GAAG,CAAC,OAAO,WAAW;EAClE;AAGD,SAAO;CACR;CAyCD,MAAM,WACJK,OACAF,aACAG,OAC6C;AAC7C,QAAM,eAAe,KAAK,UACxB,OAAM,IAAI,OACP,WACC,YACD;EAIL,MAAMC,UAAiB,CAAE;AACzB,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,kBACG,UAAU,aAAa,MAAM,AAAC,MAAc,GAAGP,oBAAM,GAAG;AAEjE,WAAQ,KACN,KAAK,SAAS,aAAa,UAAU,MAAM,KAAK,IAAIA,oBAAM,CAAC,KACzD,CAACQ,WAAgB;AAEf,QAAI,iBAAiB,OAAO,WAAW,YAAY;KAEjD,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC,OACxC,CAAC,KAAK,CAAC,KAAK,MAAM,KAAK;AACrB,UAAI,oBAAuB,QAAQ,KACjC,KAAI,OAAO;AAEb,aAAO;KACR,GACD,CAAE,EACH;AAGD,YAAO,OAAO,YAAY,MAAM,KAAK,GAAG,CAAC,OAAO,WAAW;IAC5D;AAED,WAAO;GACR,EACF,CACF;EACF;AAED,SAAO,QAAQ,IAAI,QAAQ;CAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCD,KACEC,UACAC,OACsB;AACtB,QAAM,YAAY,KAAK,OACrB,OAAM,IAAI,OACP,QACC,SACD;AAIL,SAAO,KAAK,MAAM,UAAU,SAAS,CAAE,GAAE,MAAM,KAAK,GAAG;CACxD;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"VitestTransactionIsolator-BQ5FpLtC.cjs","names":["api: TestAPI","createConnection: DatabaseConnection<TConn>","setup?: (trx: Transaction) => Promise<void>","level: IsolationLevel","testError: Error | undefined"],"sources":["../src/VitestTransactionIsolator.ts"],"sourcesContent":["import type { TestAPI } from 'vitest';\n\n/**\n * Type definition for test fixtures that provide transaction access.\n * Used with Vitest's test.extend() API to inject transactions into tests.\n *\n * @template Transaction - The transaction type specific to the database driver\n */\nexport interface DatabaseFixtures<Transaction> {\n /**\n * The database transaction available to the test.\n * All database operations should use this transaction to ensure proper rollback.\n */\n trx: Transaction;\n}\n\n/**\n * PostgreSQL transaction isolation levels.\n * Controls the visibility of concurrent transactions.\n *\n * @see https://www.postgresql.org/docs/current/transaction-iso.html\n */\nexport enum IsolationLevel {\n /**\n * Lowest isolation level. Allows dirty reads.\n * Not recommended for testing.\n */\n READ_UNCOMMITTED = 'READ UNCOMMITTED',\n /**\n * Default PostgreSQL isolation level.\n * Prevents dirty reads but allows non-repeatable reads.\n */\n READ_COMMITTED = 'READ COMMITTED',\n /**\n * Prevents dirty reads and non-repeatable reads.\n * Recommended for most test scenarios.\n */\n REPEATABLE_READ = 'REPEATABLE READ',\n /**\n * Highest isolation level. Prevents all phenomena.\n * May cause performance overhead in tests.\n */\n SERIALIZABLE = 'SERIALIZABLE',\n}\n\n/**\n * Abstract base class for implementing database transaction isolation in Vitest tests.\n * Provides automatic transaction rollback after each test to maintain test isolation.\n * Subclasses must implement the transact() method for their specific database driver.\n *\n * @template TConn - The database connection type\n * @template Transaction - The transaction type\n *\n * @example\n * ```typescript\n * // Implement for your database driver\n * class MyDatabaseIsolator extends VitestPostgresTransactionIsolator<MyDB, MyTx> {\n * async transact(conn: MyDB, level: IsolationLevel, fn: (tx: MyTx) => Promise<void>) {\n * await conn.transaction(level, fn);\n * }\n * }\n *\n * // Use in tests\n * const isolator = new MyDatabaseIsolator(test);\n * const isolatedTest = isolator.wrapVitestWithTransaction(db);\n *\n * isolatedTest('should create user', async ({ trx }) => {\n * await trx.insert('users', { name: 'Test' });\n * // Data is automatically rolled back after test\n * });\n * ```\n */\nexport abstract class VitestPostgresTransactionIsolator<TConn, Transaction> {\n /**\n * Abstract method to create a transaction with the specified isolation level.\n * Must be implemented by subclasses for specific database drivers.\n *\n * @param conn - The database connection\n * @param isolationLevel - The transaction isolation level\n * @param fn - The function to execute within the transaction\n * @returns Promise that resolves when the transaction completes\n */\n abstract transact(\n conn: TConn,\n isolationLevel: IsolationLevel,\n fn: (trx: Transaction) => Promise<void>,\n ): Promise<void>;\n\n abstract destroy(conn: TConn): Promise<void>;\n /**\n * Creates a new VitestPostgresTransactionIsolator instance.\n *\n * @param api - The Vitest test API (usually the `test` export from vitest)\n */\n constructor(private readonly api: TestAPI) {}\n\n /**\n * Creates a wrapped version of Vitest's test API that provides transaction isolation.\n * Each test will run within a database transaction that is automatically rolled back.\n *\n * @param conn - The database connection to use\n * @param setup - Optional setup function to run within the transaction before each test\n * @param level - The transaction isolation level (defaults to REPEATABLE_READ)\n * @returns A wrapped test API with transaction support\n *\n * @example\n * ```typescript\n * const isolatedTest = isolator.wrapVitestWithTransaction(db, async (trx) => {\n * // Optional setup: create common test data\n * await trx.insert('settings', { key: 'test', value: 'true' });\n * });\n *\n * isolatedTest('test with transaction', async ({ trx }) => {\n * const user = await trx.insert('users', { name: 'Test' });\n * expect(user).toBeDefined();\n * });\n * ```\n */\n wrapVitestWithTransaction(\n createConnection: DatabaseConnection<TConn>,\n setup?: (trx: Transaction) => Promise<void>,\n level: IsolationLevel = IsolationLevel.REPEATABLE_READ,\n ) {\n return this.api.extend<DatabaseFixtures<Transaction>>({\n // This fixture automatically provides a transaction to each test\n trx: async ({}, use) => {\n // Create a custom error class for rollback\n class TestRollback extends Error {\n constructor() {\n super('Test rollback');\n this.name = 'TestRollback';\n }\n }\n\n let testError: Error | undefined;\n const conn = await createConnection();\n try {\n await this.transact(conn, level, async (transaction) => {\n try {\n // Provide the transaction to the test\n await setup?.(transaction);\n await use(transaction);\n } catch (error) {\n // Capture any test errors\n testError = error as Error;\n }\n\n // Always throw to trigger rollback\n throw new TestRollback();\n });\n } catch (error) {\n // Only rethrow if it's not our rollback error\n if (!(error instanceof TestRollback)) {\n throw error;\n }\n\n // If the test had an error, throw it now\n if (testError) {\n throw testError;\n }\n } finally {\n await this.destroy(conn);\n }\n },\n });\n }\n}\n\nexport type DatabaseConnectionFn<Conn> = () => Conn | Promise<Conn>;\nexport type DatabaseConnection<Conn> = DatabaseConnectionFn<Conn>;\n"],"mappings":";;;;;;;;AAsBA,IAAY,4DAAL;;;;;AAKL;;;;;AAKA;;;;;AAKA;;;;;AAKA;;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BD,IAAsB,oCAAtB,MAA4E;;;;;;CAsB1E,YAA6BA,KAAc;EAAd;CAAgB;;;;;;;;;;;;;;;;;;;;;;;CAwB7C,0BACEC,kBACAC,OACAC,QAAwB,eAAe,iBACvC;AACA,SAAO,KAAK,IAAI,OAAsC,EAEpD,KAAK,OAAO,EAAE,EAAE,QAAQ;GAEtB,MAAM,qBAAqB,MAAM;IAC/B,cAAc;AACZ,WAAM,gBAAgB;AACtB,UAAK,OAAO;IACb;GACF;GAED,IAAIC;GACJ,MAAM,OAAO,MAAM,kBAAkB;AACrC,OAAI;AACF,UAAM,KAAK,SAAS,MAAM,OAAO,OAAO,gBAAgB;AACtD,SAAI;AAEF,YAAM,QAAQ,YAAY;AAC1B,YAAM,IAAI,YAAY;KACvB,SAAQ,OAAO;AAEd,kBAAY;KACb;AAGD,WAAM,IAAI;IACX,EAAC;GACH,SAAQ,OAAO;AAEd,UAAM,iBAAiB,cACrB,OAAM;AAIR,QAAI,UACF,OAAM;GAET,UAAS;AACR,UAAM,KAAK,QAAQ,KAAK;GACzB;EACF,EACF,EAAC;CACH;AACF"}
|