@naturalcycles/abba 2.0.1 → 2.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/dist/abba.d.ts +12 -9
- package/dist/abba.js +98 -101
- package/dist/dao/bucket.dao.d.ts +3 -3
- package/dist/dao/bucket.dao.js +20 -15
- package/dist/dao/experiment.dao.d.ts +8 -4
- package/dist/dao/experiment.dao.js +39 -28
- package/dist/dao/userAssignment.dao.d.ts +5 -1
- package/dist/dao/userAssignment.dao.js +21 -4
- package/dist/migrations/init.sql +1 -0
- package/dist/types.d.ts +16 -11
- package/dist/util.d.ts +8 -8
- package/dist/util.js +18 -18
- package/package.json +3 -2
- package/src/abba.ts +122 -105
- package/src/dao/bucket.dao.ts +9 -5
- package/src/dao/experiment.dao.ts +26 -9
- package/src/dao/userAssignment.dao.ts +29 -3
- package/src/migrations/init.sql +1 -0
- package/src/types.ts +16 -12
- package/src/util.ts +16 -18
package/dist/abba.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Unsaved } from '@naturalcycles/js-lib';
|
|
2
|
-
import
|
|
2
|
+
import { type GetAllExperimentsOpts } from './dao/experiment.dao.js';
|
|
3
|
+
import type { AbbaConfig, Bucket, DecoratedUserAssignment, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, SegmentationData, UserExperiment } from './types.js';
|
|
3
4
|
export declare class Abba {
|
|
4
5
|
cfg: AbbaConfig;
|
|
5
6
|
private experimentDao;
|
|
@@ -10,15 +11,16 @@ export declare class Abba {
|
|
|
10
11
|
* Returns all experiments.
|
|
11
12
|
* Cached (see CACHE_TTL)
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
getAllExperimentsWithBuckets(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Returns all experiments.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
getAllExperimentsWithBucketsNoCache(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
|
|
19
|
+
getUserExperiments(userId: string): Promise<UserExperiment[]>;
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
21
|
+
* Updates all user assignments with a given userId with the provided userId.
|
|
20
22
|
*/
|
|
21
|
-
|
|
23
|
+
updateUserId(oldId: string, newId: string): Promise<void>;
|
|
22
24
|
/**
|
|
23
25
|
* Creates a new experiment.
|
|
24
26
|
* Cold method.
|
|
@@ -33,6 +35,7 @@ export declare class Abba {
|
|
|
33
35
|
* Ensures that mutual exclusions are maintained
|
|
34
36
|
*/
|
|
35
37
|
private updateExclusions;
|
|
38
|
+
softDeleteExperiment(experimentId: string): Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* Delete an experiment. Removes all user assignments and buckets.
|
|
38
41
|
* Requires the experiment to have been inactive for at least 15 minutes in order to
|
|
@@ -49,20 +52,20 @@ export declare class Abba {
|
|
|
49
52
|
* @param existingOnly Do not generate any new assignments for this experiment
|
|
50
53
|
* @param segmentationData Required if existingOnly is false
|
|
51
54
|
*/
|
|
52
|
-
getUserAssignment(experimentKey: string, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<
|
|
55
|
+
getUserAssignment(experimentKey: string, userId: string, existingOnly: boolean, segmentationData?: SegmentationData): Promise<DecoratedUserAssignment | null>;
|
|
53
56
|
/**
|
|
54
57
|
* Get all existing user assignments.
|
|
55
58
|
* Hot method.
|
|
56
59
|
* Not cached, because Assignments are fast-changing.
|
|
57
60
|
* Only to be used for testing
|
|
58
61
|
*/
|
|
59
|
-
|
|
62
|
+
getAllExistingUserAssignments(userId: string): Promise<DecoratedUserAssignment[]>;
|
|
60
63
|
/**
|
|
61
64
|
* Generate user assignments for all active experiments.
|
|
62
65
|
* Will return any existing and attempt to generate any new assignments if existingOnly is false.
|
|
63
66
|
* Hot method.
|
|
64
67
|
*/
|
|
65
|
-
generateUserAssignments(userId: string, segmentationData: SegmentationData, existingOnly?: boolean): Promise<
|
|
68
|
+
generateUserAssignments(userId: string, segmentationData: SegmentationData, existingOnly?: boolean): Promise<DecoratedUserAssignment[]>;
|
|
66
69
|
/**
|
|
67
70
|
* Get assignment statistics for an experiment.
|
|
68
71
|
* Cold method.
|
package/dist/abba.js
CHANGED
|
@@ -21,27 +21,49 @@ export class Abba {
|
|
|
21
21
|
* Returns all experiments.
|
|
22
22
|
* Cached (see CACHE_TTL)
|
|
23
23
|
*/
|
|
24
|
-
async
|
|
25
|
-
return await this.
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Updates all user assignments with a given userId with the provided userId.
|
|
29
|
-
*/
|
|
30
|
-
async updateUserId(oldId, newId) {
|
|
31
|
-
const query = this.userAssignmentDao.query().filterEq('userId', oldId);
|
|
32
|
-
await this.userAssignmentDao.patchByQuery(query, { userId: newId });
|
|
24
|
+
async getAllExperimentsWithBuckets(opts) {
|
|
25
|
+
return await this.getAllExperimentsWithBucketsNoCache(opts);
|
|
33
26
|
}
|
|
34
27
|
/**
|
|
35
28
|
* Returns all experiments.
|
|
36
29
|
*/
|
|
37
|
-
async
|
|
38
|
-
const experiments = await this.experimentDao.
|
|
30
|
+
async getAllExperimentsWithBucketsNoCache(opts) {
|
|
31
|
+
const experiments = await this.experimentDao.getAllExperiments(opts);
|
|
39
32
|
const buckets = await this.bucketDao.getAll();
|
|
40
33
|
return experiments.map(experiment => ({
|
|
41
34
|
...experiment,
|
|
42
35
|
buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
|
|
43
36
|
}));
|
|
44
37
|
}
|
|
38
|
+
async getUserExperiments(userId) {
|
|
39
|
+
const experiments = await this.getAllExperimentsWithBuckets({ includeDeleted: false });
|
|
40
|
+
const experimentIds = experiments.map(e => e.id);
|
|
41
|
+
const userAssignments = await this.userAssignmentDao.getUserAssigmentsByExperimentIds(userId, experimentIds);
|
|
42
|
+
return experiments.map(experiment => {
|
|
43
|
+
const existingAssignment = userAssignments.find(ua => ua.experimentId === experiment.id);
|
|
44
|
+
const existingAssignmentBucket = experiment.buckets.find(b => b.id === existingAssignment?.bucketId);
|
|
45
|
+
return {
|
|
46
|
+
...experiment,
|
|
47
|
+
...(existingAssignment && {
|
|
48
|
+
userAssignment: {
|
|
49
|
+
...existingAssignment,
|
|
50
|
+
experimentId: experiment.id,
|
|
51
|
+
experimentData: experiment.data,
|
|
52
|
+
experimentKey: experiment.key,
|
|
53
|
+
bucketData: existingAssignmentBucket?.data || null,
|
|
54
|
+
bucketKey: existingAssignmentBucket?.key || null,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Updates all user assignments with a given userId with the provided userId.
|
|
62
|
+
*/
|
|
63
|
+
async updateUserId(oldId, newId) {
|
|
64
|
+
const query = this.userAssignmentDao.query().filterEq('userId', oldId);
|
|
65
|
+
await this.userAssignmentDao.patchByQuery(query, { userId: newId });
|
|
66
|
+
}
|
|
45
67
|
/**
|
|
46
68
|
* Creates a new experiment.
|
|
47
69
|
* Cold method.
|
|
@@ -98,6 +120,10 @@ export class Abba {
|
|
|
98
120
|
});
|
|
99
121
|
await this.experimentDao.saveBatch(requiresUpdating, { saveMethod: 'update' });
|
|
100
122
|
}
|
|
123
|
+
async softDeleteExperiment(experimentId) {
|
|
124
|
+
await this.experimentDao.patchById(experimentId, { deleted: true, exclusions: [] });
|
|
125
|
+
await this.updateExclusions(experimentId, []);
|
|
126
|
+
}
|
|
101
127
|
/**
|
|
102
128
|
* Delete an experiment. Removes all user assignments and buckets.
|
|
103
129
|
* Requires the experiment to have been inactive for at least 15 minutes in order to
|
|
@@ -128,64 +154,47 @@ export class Abba {
|
|
|
128
154
|
* @param segmentationData Required if existingOnly is false
|
|
129
155
|
*/
|
|
130
156
|
async getUserAssignment(experimentKey, userId, existingOnly, segmentationData) {
|
|
131
|
-
const experiment = await this.experimentDao.
|
|
157
|
+
const experiment = await this.experimentDao.getByKey(experimentKey);
|
|
132
158
|
_assert(experiment, `Experiment does not exist: ${experimentKey}`);
|
|
133
159
|
// Inactive experiments should never return an assignment
|
|
134
160
|
if (experiment.status === AssignmentStatus.Inactive) {
|
|
135
|
-
return
|
|
136
|
-
experiment,
|
|
137
|
-
assignment: null,
|
|
138
|
-
};
|
|
161
|
+
return null;
|
|
139
162
|
}
|
|
140
|
-
const buckets = await this.bucketDao.
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const bucket = buckets.find(b => b.id === existing.bucketId);
|
|
163
|
+
const buckets = await this.bucketDao.getByExperimentId(experiment.id);
|
|
164
|
+
const userAssignment = await this.userAssignmentDao.getUserAssignmentByExperimentId(userId, experiment.id);
|
|
165
|
+
if (userAssignment) {
|
|
166
|
+
const bucket = buckets.find(b => b.id === userAssignment.bucketId);
|
|
145
167
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
bucketData: bucket?.data || null,
|
|
152
|
-
},
|
|
168
|
+
...userAssignment,
|
|
169
|
+
experimentData: experiment.data,
|
|
170
|
+
experimentKey: experiment.key,
|
|
171
|
+
bucketKey: bucket?.key || null,
|
|
172
|
+
bucketData: bucket?.data || null,
|
|
153
173
|
};
|
|
154
174
|
}
|
|
155
175
|
// No existing assignment, but we don't want to generate a new one
|
|
156
176
|
if (existingOnly || experiment.status === AssignmentStatus.Paused) {
|
|
157
|
-
return
|
|
158
|
-
experiment,
|
|
159
|
-
assignment: null,
|
|
160
|
-
};
|
|
177
|
+
return null;
|
|
161
178
|
}
|
|
162
|
-
const experiments = await this.
|
|
163
|
-
const exclusionSet = getUserExclusionSet(experiments
|
|
179
|
+
const experiments = await this.getUserExperiments(userId);
|
|
180
|
+
const exclusionSet = getUserExclusionSet(experiments);
|
|
164
181
|
if (!canGenerateNewAssignments(experiment, exclusionSet)) {
|
|
165
|
-
return
|
|
166
|
-
experiment,
|
|
167
|
-
assignment: null,
|
|
168
|
-
};
|
|
182
|
+
return null;
|
|
169
183
|
}
|
|
170
184
|
_assert(segmentationData, 'Segmentation data required when creating a new assignment');
|
|
171
185
|
const experimentWithBuckets = { ...experiment, buckets };
|
|
172
186
|
const assignment = generateUserAssignmentData(experimentWithBuckets, userId, segmentationData);
|
|
173
187
|
if (!assignment) {
|
|
174
|
-
return
|
|
175
|
-
experiment,
|
|
176
|
-
assignment: null,
|
|
177
|
-
};
|
|
188
|
+
return null;
|
|
178
189
|
}
|
|
179
190
|
const newAssignment = await this.userAssignmentDao.save(assignment);
|
|
180
191
|
const bucket = buckets.find(b => b.id === newAssignment.bucketId);
|
|
181
192
|
return {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
bucketData: bucket?.data || null,
|
|
188
|
-
},
|
|
193
|
+
...newAssignment,
|
|
194
|
+
experimentData: experiment.data,
|
|
195
|
+
experimentKey: experiment.key,
|
|
196
|
+
bucketKey: bucket?.key || null,
|
|
197
|
+
bucketData: bucket?.data || null,
|
|
189
198
|
};
|
|
190
199
|
}
|
|
191
200
|
/**
|
|
@@ -194,19 +203,17 @@ export class Abba {
|
|
|
194
203
|
* Not cached, because Assignments are fast-changing.
|
|
195
204
|
* Only to be used for testing
|
|
196
205
|
*/
|
|
197
|
-
async
|
|
206
|
+
async getAllExistingUserAssignments(userId) {
|
|
198
207
|
const assignments = await this.userAssignmentDao.getBy('userId', userId);
|
|
199
208
|
return await pMap(assignments, async (assignment) => {
|
|
200
209
|
const experiment = await this.experimentDao.requireById(assignment.experimentId);
|
|
201
210
|
const bucket = await this.bucketDao.getById(assignment.bucketId);
|
|
202
211
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
bucketData: bucket?.data || null,
|
|
209
|
-
},
|
|
212
|
+
...assignment,
|
|
213
|
+
experimentData: experiment.data,
|
|
214
|
+
experimentKey: experiment.key,
|
|
215
|
+
bucketKey: bucket?.key || null,
|
|
216
|
+
bucketData: bucket?.data || null,
|
|
210
217
|
};
|
|
211
218
|
});
|
|
212
219
|
}
|
|
@@ -216,47 +223,43 @@ export class Abba {
|
|
|
216
223
|
* Hot method.
|
|
217
224
|
*/
|
|
218
225
|
async generateUserAssignments(userId, segmentationData, existingOnly = false) {
|
|
219
|
-
const experiments = await this.
|
|
220
|
-
const
|
|
221
|
-
const exclusionSet = getUserExclusionSet(experiments, existingAssignments);
|
|
222
|
-
const assignments = [];
|
|
223
|
-
const newAssignments = [];
|
|
226
|
+
const experiments = await this.getUserExperiments(userId);
|
|
227
|
+
const exclusionSet = getUserExclusionSet(experiments);
|
|
224
228
|
// Shuffling means that randomisation occurs in the mutual exclusion
|
|
225
229
|
// as experiments are looped through sequentially, this removes the risk of the same experiment always being assigned first in the list of mutually exclusive experiments
|
|
226
230
|
// This is simmpler than trying to resolve after assignments have already been determined
|
|
227
231
|
const availableExperiments = _shuffle(experiments.filter(e => e.status === AssignmentStatus.Active || e.status === AssignmentStatus.Paused));
|
|
232
|
+
const assignments = [];
|
|
233
|
+
const newAssignments = [];
|
|
228
234
|
for (const experiment of availableExperiments) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
const { userAssignment } = experiment;
|
|
236
|
+
// Already assigned to this experiment
|
|
237
|
+
if (userAssignment) {
|
|
238
|
+
assignments.push(userAssignment);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Not already assigned, but we don't want to generate a new assignment
|
|
242
|
+
if (existingOnly)
|
|
243
|
+
continue;
|
|
244
|
+
// We are not allowed to generate new assignments for this experiment
|
|
245
|
+
if (!canGenerateNewAssignments(experiment, exclusionSet))
|
|
246
|
+
continue;
|
|
247
|
+
const assignment = generateUserAssignmentData(experiment, userId, segmentationData);
|
|
248
|
+
if (assignment) {
|
|
249
|
+
// Add to list of new assignments to be saved
|
|
250
|
+
const newAssignment = this.userAssignmentDao.create(assignment);
|
|
251
|
+
newAssignments.push(newAssignment);
|
|
252
|
+
// Add the assignment to the list of assignments
|
|
253
|
+
const bucket = experiment.buckets.find(b => b.id === assignment.bucketId);
|
|
232
254
|
assignments.push({
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
bucketData: bucket?.data || null,
|
|
239
|
-
},
|
|
255
|
+
...newAssignment,
|
|
256
|
+
experimentKey: experiment.key,
|
|
257
|
+
experimentData: experiment.data,
|
|
258
|
+
bucketKey: bucket?.key || null,
|
|
259
|
+
bucketData: bucket?.data || null,
|
|
240
260
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const assignment = generateUserAssignmentData(experiment, userId, segmentationData);
|
|
244
|
-
if (assignment) {
|
|
245
|
-
const created = this.userAssignmentDao.create(assignment);
|
|
246
|
-
newAssignments.push(created);
|
|
247
|
-
const bucket = experiment.buckets.find(b => b.id === created.bucketId);
|
|
248
|
-
assignments.push({
|
|
249
|
-
experiment,
|
|
250
|
-
assignment: {
|
|
251
|
-
...created,
|
|
252
|
-
experimentKey: experiment.key,
|
|
253
|
-
bucketKey: bucket?.key || null,
|
|
254
|
-
bucketData: bucket?.data || null,
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
// Prevent future exclusion clashes
|
|
258
|
-
experiment.exclusions.forEach(experimentId => exclusionSet.add(experimentId));
|
|
259
|
-
}
|
|
261
|
+
// Prevent future exclusion clashes
|
|
262
|
+
experiment.exclusions.forEach(experimentId => exclusionSet.add(experimentId));
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
await this.userAssignmentDao.saveBatch(newAssignments);
|
|
@@ -267,16 +270,10 @@ export class Abba {
|
|
|
267
270
|
* Cold method.
|
|
268
271
|
*/
|
|
269
272
|
async getExperimentAssignmentStatistics(experimentId) {
|
|
270
|
-
const totalAssignments = await this.userAssignmentDao
|
|
271
|
-
|
|
272
|
-
.filterEq('experimentId', experimentId)
|
|
273
|
-
.runQueryCount();
|
|
274
|
-
const buckets = await this.bucketDao.getBy('experimentId', experimentId);
|
|
273
|
+
const totalAssignments = await this.userAssignmentDao.getCountByExperimentId(experimentId);
|
|
274
|
+
const buckets = await this.bucketDao.getByExperimentId(experimentId);
|
|
275
275
|
const bucketAssignments = await pMap(buckets, async (bucket) => {
|
|
276
|
-
const totalAssignments = await this.userAssignmentDao
|
|
277
|
-
.query()
|
|
278
|
-
.filterEq('bucketId', bucket.id)
|
|
279
|
-
.runQueryCount();
|
|
276
|
+
const totalAssignments = await this.userAssignmentDao.getCountByBucketId(bucket.id);
|
|
280
277
|
return {
|
|
281
278
|
bucketId: bucket.id,
|
|
282
279
|
totalAssignments,
|
|
@@ -290,4 +287,4 @@ export class Abba {
|
|
|
290
287
|
}
|
|
291
288
|
__decorate([
|
|
292
289
|
_Memo({ cacheFactory: () => new LRUMemoCache({ ttl: CACHE_TTL, max: 1 }) })
|
|
293
|
-
], Abba.prototype, "
|
|
290
|
+
], Abba.prototype, "getAllExperimentsWithBuckets", null);
|
package/dist/dao/bucket.dao.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib';
|
|
2
2
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
3
|
-
import type { Saved } from '@naturalcycles/js-lib';
|
|
4
3
|
import type { BaseBucket, Bucket } from '../types.js';
|
|
5
|
-
type BucketDBM =
|
|
4
|
+
type BucketDBM = BaseBucket & {
|
|
6
5
|
data: string | null;
|
|
7
6
|
};
|
|
8
7
|
export declare class BucketDao extends CommonDao<Bucket, BucketDBM> {
|
|
8
|
+
getByExperimentId(experimentId: string): Promise<Bucket[]>;
|
|
9
9
|
}
|
|
10
|
-
export declare
|
|
10
|
+
export declare function bucketDao(db: CommonDB): BucketDao;
|
|
11
11
|
export {};
|
package/dist/dao/bucket.dao.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
2
2
|
export class BucketDao extends CommonDao {
|
|
3
|
+
async getByExperimentId(experimentId) {
|
|
4
|
+
return await this.query().filterEq('experimentId', experimentId).runQuery();
|
|
5
|
+
}
|
|
3
6
|
}
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
export function bucketDao(db) {
|
|
8
|
+
return new BucketDao({
|
|
9
|
+
db,
|
|
10
|
+
table: 'Bucket',
|
|
11
|
+
hooks: {
|
|
12
|
+
beforeBMToDBM: bm => {
|
|
13
|
+
return {
|
|
14
|
+
...bm,
|
|
15
|
+
data: bm.data ? JSON.stringify(bm.data) : null,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
beforeDBMToBM: dbm => ({
|
|
19
|
+
...dbm,
|
|
20
|
+
data: dbm.data ? JSON.parse(dbm.data) : null,
|
|
21
|
+
}),
|
|
13
22
|
},
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
data: dbm.data ? JSON.parse(dbm.data) : null,
|
|
17
|
-
}),
|
|
18
|
-
},
|
|
19
|
-
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib';
|
|
2
2
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
3
|
-
import type { Saved } from '@naturalcycles/js-lib';
|
|
4
3
|
import type { BaseExperiment, Experiment } from '../types.js';
|
|
5
|
-
|
|
4
|
+
export declare class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
|
|
5
|
+
getAllExperiments(opt?: GetAllExperimentsOpts): Promise<Experiment[]>;
|
|
6
|
+
getByKey(key: string): Promise<Experiment | null>;
|
|
7
|
+
}
|
|
8
|
+
export declare function experimentDao(db: CommonDB): ExperimentDao;
|
|
9
|
+
type ExperimentDBM = BaseExperiment & {
|
|
6
10
|
rules: string | null;
|
|
7
11
|
exclusions: string | null;
|
|
8
12
|
data: string | null;
|
|
9
13
|
};
|
|
10
|
-
export
|
|
14
|
+
export interface GetAllExperimentsOpts {
|
|
15
|
+
includeDeleted?: boolean;
|
|
11
16
|
}
|
|
12
|
-
export declare const experimentDao: (db: CommonDB) => ExperimentDao;
|
|
13
17
|
export {};
|
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
2
2
|
import { localDate } from '@naturalcycles/js-lib';
|
|
3
3
|
export class ExperimentDao extends CommonDao {
|
|
4
|
+
async getAllExperiments(opt) {
|
|
5
|
+
if (!opt?.includeDeleted) {
|
|
6
|
+
return await this.getAll();
|
|
7
|
+
}
|
|
8
|
+
return await this.query().filterEq('deleted', false).runQuery();
|
|
9
|
+
}
|
|
10
|
+
async getByKey(key) {
|
|
11
|
+
return await this.getOneBy('key', key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function experimentDao(db) {
|
|
15
|
+
return new ExperimentDao({
|
|
16
|
+
db,
|
|
17
|
+
table: 'Experiment',
|
|
18
|
+
hooks: {
|
|
19
|
+
beforeBMToDBM: bm => ({
|
|
20
|
+
...bm,
|
|
21
|
+
rules: bm.rules.length ? JSON.stringify(bm.rules) : null,
|
|
22
|
+
// We add the map here to account for backwards compatibility where exclusion experimentIds were stored as a number
|
|
23
|
+
// TODO: Remove after some time when we are certain only strings are stored
|
|
24
|
+
exclusions: bm.exclusions.length
|
|
25
|
+
? JSON.stringify(bm.exclusions.map(exclusion => exclusion.toString()))
|
|
26
|
+
: null,
|
|
27
|
+
data: bm.data ? JSON.stringify(bm.data) : null,
|
|
28
|
+
}),
|
|
29
|
+
beforeDBMToBM: dbm => ({
|
|
30
|
+
...dbm,
|
|
31
|
+
startDateIncl: parseMySQLDate(dbm.startDateIncl),
|
|
32
|
+
endDateExcl: parseMySQLDate(dbm.endDateExcl),
|
|
33
|
+
rules: (dbm.rules && JSON.parse(dbm.rules)) || [],
|
|
34
|
+
// We add the map here to account for backwards compatibility where exclusion experimentIds were stored as a number
|
|
35
|
+
// TODO: Remove after some time when we are certain only strings are stored
|
|
36
|
+
exclusions: (dbm.exclusions &&
|
|
37
|
+
JSON.parse(dbm.exclusions).map((exclusion) => exclusion.toString())) ||
|
|
38
|
+
[],
|
|
39
|
+
data: dbm.data ? JSON.parse(dbm.data) : null,
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
4
43
|
}
|
|
5
|
-
export const experimentDao = (db) => new ExperimentDao({
|
|
6
|
-
db,
|
|
7
|
-
table: 'Experiment',
|
|
8
|
-
hooks: {
|
|
9
|
-
beforeBMToDBM: bm => ({
|
|
10
|
-
...bm,
|
|
11
|
-
rules: bm.rules.length ? JSON.stringify(bm.rules) : null,
|
|
12
|
-
// We add the map here to account for backwards compatibility where exclusion experimentIds were stored as a number
|
|
13
|
-
// TODO: Remove after some time when we are certain only strings are stored
|
|
14
|
-
exclusions: bm.exclusions.length
|
|
15
|
-
? JSON.stringify(bm.exclusions.map(exclusion => exclusion.toString()))
|
|
16
|
-
: null,
|
|
17
|
-
data: bm.data ? JSON.stringify(bm.data) : null,
|
|
18
|
-
}),
|
|
19
|
-
beforeDBMToBM: dbm => ({
|
|
20
|
-
...dbm,
|
|
21
|
-
startDateIncl: parseMySQLDate(dbm.startDateIncl),
|
|
22
|
-
endDateExcl: parseMySQLDate(dbm.endDateExcl),
|
|
23
|
-
rules: (dbm.rules && JSON.parse(dbm.rules)) || [],
|
|
24
|
-
// We add the map here to account for backwards compatibility where exclusion experimentIds were stored as a number
|
|
25
|
-
// TODO: Remove after some time when we are certain only strings are stored
|
|
26
|
-
exclusions: (dbm.exclusions &&
|
|
27
|
-
JSON.parse(dbm.exclusions).map((exclusion) => exclusion.toString())) ||
|
|
28
|
-
[],
|
|
29
|
-
data: dbm.data ? JSON.parse(dbm.data) : null,
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
44
|
/**
|
|
34
45
|
* https://nc1.slack.com/archives/CCNTHJT7V/p1682514277002739
|
|
35
46
|
* MySQL Automatically parses Date fields as Date objects
|
|
@@ -2,5 +2,9 @@ import type { CommonDB } from '@naturalcycles/db-lib';
|
|
|
2
2
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
3
3
|
import type { UserAssignment } from '../types.js';
|
|
4
4
|
export declare class UserAssignmentDao extends CommonDao<UserAssignment> {
|
|
5
|
+
getUserAssignmentByExperimentId(userId: string, experimentId: string): Promise<UserAssignment | null>;
|
|
6
|
+
getUserAssigmentsByExperimentIds(userId: string, experimentIds: string[]): Promise<UserAssignment[]>;
|
|
7
|
+
getCountByExperimentId(experimentId: string): Promise<number>;
|
|
8
|
+
getCountByBucketId(bucketId: string): Promise<number>;
|
|
5
9
|
}
|
|
6
|
-
export declare
|
|
10
|
+
export declare function userAssignmentDao(db: CommonDB): UserAssignmentDao;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
2
2
|
export class UserAssignmentDao extends CommonDao {
|
|
3
|
+
async getUserAssignmentByExperimentId(userId, experimentId) {
|
|
4
|
+
const query = this.query().filterEq('userId', userId).filterEq('experimentId', experimentId);
|
|
5
|
+
const [userAssignment] = await this.runQuery(query);
|
|
6
|
+
return userAssignment || null;
|
|
7
|
+
}
|
|
8
|
+
async getUserAssigmentsByExperimentIds(userId, experimentIds) {
|
|
9
|
+
const query = this.query().filterEq('userId', userId).filterIn('experimentId', experimentIds);
|
|
10
|
+
return await this.runQuery(query);
|
|
11
|
+
}
|
|
12
|
+
async getCountByExperimentId(experimentId) {
|
|
13
|
+
return await this.query().filterEq('experimentId', experimentId).runQueryCount();
|
|
14
|
+
}
|
|
15
|
+
async getCountByBucketId(bucketId) {
|
|
16
|
+
return await this.query().filterEq('bucketId', bucketId).runQueryCount();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function userAssignmentDao(db) {
|
|
20
|
+
return new UserAssignmentDao({
|
|
21
|
+
db,
|
|
22
|
+
table: 'UserAssignment',
|
|
23
|
+
});
|
|
3
24
|
}
|
|
4
|
-
export const userAssignmentDao = (db) => new UserAssignmentDao({
|
|
5
|
-
db,
|
|
6
|
-
table: 'UserAssignment',
|
|
7
|
-
});
|
package/dist/migrations/init.sql
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib';
|
|
2
|
-
import type { AnyObject, BaseDBEntity, IsoDate
|
|
2
|
+
import type { AnyObject, BaseDBEntity, IsoDate } from '@naturalcycles/js-lib';
|
|
3
3
|
export interface AbbaConfig {
|
|
4
4
|
db: CommonDB;
|
|
5
5
|
}
|
|
@@ -29,14 +29,18 @@ export type BaseExperiment = BaseDBEntity & {
|
|
|
29
29
|
* Date range end for the experiment assignments
|
|
30
30
|
*/
|
|
31
31
|
endDateExcl: IsoDate;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the experiment is flagged as deleted. This acts as a soft delete only.
|
|
34
|
+
*/
|
|
35
|
+
deleted: boolean;
|
|
32
36
|
};
|
|
33
37
|
export type Experiment = BaseExperiment & {
|
|
34
38
|
rules: SegmentationRule[];
|
|
35
39
|
exclusions: string[];
|
|
36
40
|
data: AnyObject | null;
|
|
37
41
|
};
|
|
38
|
-
export type ExperimentWithBuckets =
|
|
39
|
-
buckets:
|
|
42
|
+
export type ExperimentWithBuckets = Experiment & {
|
|
43
|
+
buckets: Bucket[];
|
|
40
44
|
};
|
|
41
45
|
export type BaseBucket = BaseDBEntity & {
|
|
42
46
|
experimentId: string;
|
|
@@ -51,14 +55,12 @@ export type UserAssignment = BaseDBEntity & {
|
|
|
51
55
|
experimentId: string;
|
|
52
56
|
bucketId: string | null;
|
|
53
57
|
};
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
experiment: Saved<Experiment>;
|
|
61
|
-
}
|
|
58
|
+
export type DecoratedUserAssignment = UserAssignment & {
|
|
59
|
+
experimentKey: Experiment['key'];
|
|
60
|
+
experimentData: Experiment['data'];
|
|
61
|
+
bucketKey: Bucket['key'] | null;
|
|
62
|
+
bucketData: Bucket['data'];
|
|
63
|
+
};
|
|
62
64
|
export type SegmentationData = AnyObject;
|
|
63
65
|
export declare enum AssignmentStatus {
|
|
64
66
|
/**
|
|
@@ -105,3 +107,6 @@ export interface BucketAssignmentStatistics {
|
|
|
105
107
|
totalAssignments: number;
|
|
106
108
|
}
|
|
107
109
|
export type ExclusionSet = Set<string>;
|
|
110
|
+
export interface UserExperiment extends ExperimentWithBuckets {
|
|
111
|
+
userAssignment?: DecoratedUserAssignment;
|
|
112
|
+
}
|