@naturalcycles/abba 2.0.2 → 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 +8 -6
- package/dist/abba.js +21 -17
- package/dist/dao/experiment.dao.d.ts +7 -3
- package/dist/dao/experiment.dao.js +6 -0
- package/dist/migrations/init.sql +1 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/abba.ts +29 -20
- package/src/dao/experiment.dao.ts +18 -6
- package/src/migrations/init.sql +1 -0
- package/src/types.ts +4 -0
package/dist/abba.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Unsaved } from '@naturalcycles/js-lib';
|
|
2
|
+
import { type GetAllExperimentsOpts } from './dao/experiment.dao.js';
|
|
2
3
|
import type { AbbaConfig, Bucket, DecoratedUserAssignment, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, SegmentationData, UserExperiment } from './types.js';
|
|
3
4
|
export declare class Abba {
|
|
4
5
|
cfg: AbbaConfig;
|
|
@@ -10,16 +11,16 @@ export declare class Abba {
|
|
|
10
11
|
* Returns all experiments.
|
|
11
12
|
* Cached (see CACHE_TTL)
|
|
12
13
|
*/
|
|
13
|
-
getAllExperimentsWithBuckets(): Promise<ExperimentWithBuckets[]>;
|
|
14
|
-
getAllExperimentsWithUserAssignments(userId: string): Promise<UserExperiment[]>;
|
|
14
|
+
getAllExperimentsWithBuckets(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Returns all experiments.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
getAllExperimentsWithBucketsNoCache(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
|
|
19
|
+
getUserExperiments(userId: string): Promise<UserExperiment[]>;
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Updates all user assignments with a given userId with the provided userId.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
+
updateUserId(oldId: string, newId: string): Promise<void>;
|
|
23
24
|
/**
|
|
24
25
|
* Creates a new experiment.
|
|
25
26
|
* Cold method.
|
|
@@ -34,6 +35,7 @@ export declare class Abba {
|
|
|
34
35
|
* Ensures that mutual exclusions are maintained
|
|
35
36
|
*/
|
|
36
37
|
private updateExclusions;
|
|
38
|
+
softDeleteExperiment(experimentId: string): Promise<void>;
|
|
37
39
|
/**
|
|
38
40
|
* Delete an experiment. Removes all user assignments and buckets.
|
|
39
41
|
* Requires the experiment to have been inactive for at least 15 minutes in order to
|
package/dist/abba.js
CHANGED
|
@@ -21,11 +21,22 @@ export class Abba {
|
|
|
21
21
|
* Returns all experiments.
|
|
22
22
|
* Cached (see CACHE_TTL)
|
|
23
23
|
*/
|
|
24
|
-
async getAllExperimentsWithBuckets() {
|
|
25
|
-
return await this.getAllExperimentsWithBucketsNoCache();
|
|
24
|
+
async getAllExperimentsWithBuckets(opts) {
|
|
25
|
+
return await this.getAllExperimentsWithBucketsNoCache(opts);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Returns all experiments.
|
|
29
|
+
*/
|
|
30
|
+
async getAllExperimentsWithBucketsNoCache(opts) {
|
|
31
|
+
const experiments = await this.experimentDao.getAllExperiments(opts);
|
|
32
|
+
const buckets = await this.bucketDao.getAll();
|
|
33
|
+
return experiments.map(experiment => ({
|
|
34
|
+
...experiment,
|
|
35
|
+
buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
async getUserExperiments(userId) {
|
|
39
|
+
const experiments = await this.getAllExperimentsWithBuckets({ includeDeleted: false });
|
|
29
40
|
const experimentIds = experiments.map(e => e.id);
|
|
30
41
|
const userAssignments = await this.userAssignmentDao.getUserAssigmentsByExperimentIds(userId, experimentIds);
|
|
31
42
|
return experiments.map(experiment => {
|
|
@@ -53,17 +64,6 @@ export class Abba {
|
|
|
53
64
|
const query = this.userAssignmentDao.query().filterEq('userId', oldId);
|
|
54
65
|
await this.userAssignmentDao.patchByQuery(query, { userId: newId });
|
|
55
66
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Returns all experiments.
|
|
58
|
-
*/
|
|
59
|
-
async getAllExperimentsWithBucketsNoCache() {
|
|
60
|
-
const experiments = await this.experimentDao.getAll();
|
|
61
|
-
const buckets = await this.bucketDao.getAll();
|
|
62
|
-
return experiments.map(experiment => ({
|
|
63
|
-
...experiment,
|
|
64
|
-
buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
67
|
/**
|
|
68
68
|
* Creates a new experiment.
|
|
69
69
|
* Cold method.
|
|
@@ -120,6 +120,10 @@ export class Abba {
|
|
|
120
120
|
});
|
|
121
121
|
await this.experimentDao.saveBatch(requiresUpdating, { saveMethod: 'update' });
|
|
122
122
|
}
|
|
123
|
+
async softDeleteExperiment(experimentId) {
|
|
124
|
+
await this.experimentDao.patchById(experimentId, { deleted: true, exclusions: [] });
|
|
125
|
+
await this.updateExclusions(experimentId, []);
|
|
126
|
+
}
|
|
123
127
|
/**
|
|
124
128
|
* Delete an experiment. Removes all user assignments and buckets.
|
|
125
129
|
* Requires the experiment to have been inactive for at least 15 minutes in order to
|
|
@@ -172,7 +176,7 @@ export class Abba {
|
|
|
172
176
|
if (existingOnly || experiment.status === AssignmentStatus.Paused) {
|
|
173
177
|
return null;
|
|
174
178
|
}
|
|
175
|
-
const experiments = await this.
|
|
179
|
+
const experiments = await this.getUserExperiments(userId);
|
|
176
180
|
const exclusionSet = getUserExclusionSet(experiments);
|
|
177
181
|
if (!canGenerateNewAssignments(experiment, exclusionSet)) {
|
|
178
182
|
return null;
|
|
@@ -219,7 +223,7 @@ export class Abba {
|
|
|
219
223
|
* Hot method.
|
|
220
224
|
*/
|
|
221
225
|
async generateUserAssignments(userId, segmentationData, existingOnly = false) {
|
|
222
|
-
const experiments = await this.
|
|
226
|
+
const experiments = await this.getUserExperiments(userId);
|
|
223
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
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { CommonDB } from '@naturalcycles/db-lib';
|
|
2
2
|
import { CommonDao } from '@naturalcycles/db-lib';
|
|
3
3
|
import type { BaseExperiment, Experiment } from '../types.js';
|
|
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;
|
|
4
9
|
type ExperimentDBM = BaseExperiment & {
|
|
5
10
|
rules: string | null;
|
|
6
11
|
exclusions: string | null;
|
|
7
12
|
data: string | null;
|
|
8
13
|
};
|
|
9
|
-
export
|
|
10
|
-
|
|
14
|
+
export interface GetAllExperimentsOpts {
|
|
15
|
+
includeDeleted?: boolean;
|
|
11
16
|
}
|
|
12
|
-
export declare function experimentDao(db: CommonDB): ExperimentDao;
|
|
13
17
|
export {};
|
|
@@ -1,6 +1,12 @@
|
|
|
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
|
+
}
|
|
4
10
|
async getByKey(key) {
|
|
5
11
|
return await this.getOneBy('key', key);
|
|
6
12
|
}
|
package/dist/migrations/init.sql
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ 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[];
|
package/package.json
CHANGED
package/src/abba.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Unsaved } from '@naturalcycles/js-lib'
|
|
|
2
2
|
import { _assert, _Memo, _shuffle, localTime, pMap } from '@naturalcycles/js-lib'
|
|
3
3
|
import { LRUMemoCache } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import { bucketDao } from './dao/bucket.dao.js'
|
|
5
|
-
import { experimentDao } from './dao/experiment.dao.js'
|
|
5
|
+
import { experimentDao, type GetAllExperimentsOpts } from './dao/experiment.dao.js'
|
|
6
6
|
import { userAssignmentDao } from './dao/userAssignment.dao.js'
|
|
7
7
|
import type {
|
|
8
8
|
AbbaConfig,
|
|
@@ -41,12 +41,29 @@ export class Abba {
|
|
|
41
41
|
* Cached (see CACHE_TTL)
|
|
42
42
|
*/
|
|
43
43
|
@_Memo({ cacheFactory: () => new LRUMemoCache({ ttl: CACHE_TTL, max: 1 }) })
|
|
44
|
-
async getAllExperimentsWithBuckets(
|
|
45
|
-
|
|
44
|
+
async getAllExperimentsWithBuckets(
|
|
45
|
+
opts?: GetAllExperimentsOpts,
|
|
46
|
+
): Promise<ExperimentWithBuckets[]> {
|
|
47
|
+
return await this.getAllExperimentsWithBucketsNoCache(opts)
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Returns all experiments.
|
|
52
|
+
*/
|
|
53
|
+
async getAllExperimentsWithBucketsNoCache(
|
|
54
|
+
opts?: GetAllExperimentsOpts,
|
|
55
|
+
): Promise<ExperimentWithBuckets[]> {
|
|
56
|
+
const experiments = await this.experimentDao.getAllExperiments(opts)
|
|
57
|
+
const buckets = await this.bucketDao.getAll()
|
|
58
|
+
|
|
59
|
+
return experiments.map(experiment => ({
|
|
60
|
+
...experiment,
|
|
61
|
+
buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getUserExperiments(userId: string): Promise<UserExperiment[]> {
|
|
66
|
+
const experiments = await this.getAllExperimentsWithBuckets({ includeDeleted: false })
|
|
50
67
|
|
|
51
68
|
const experimentIds = experiments.map(e => e.id)
|
|
52
69
|
const userAssignments = await this.userAssignmentDao.getUserAssigmentsByExperimentIds(
|
|
@@ -84,19 +101,6 @@ export class Abba {
|
|
|
84
101
|
await this.userAssignmentDao.patchByQuery(query, { userId: newId })
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
/**
|
|
88
|
-
* Returns all experiments.
|
|
89
|
-
*/
|
|
90
|
-
async getAllExperimentsWithBucketsNoCache(): Promise<ExperimentWithBuckets[]> {
|
|
91
|
-
const experiments = await this.experimentDao.getAll()
|
|
92
|
-
const buckets = await this.bucketDao.getAll()
|
|
93
|
-
|
|
94
|
-
return experiments.map(experiment => ({
|
|
95
|
-
...experiment,
|
|
96
|
-
buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
|
|
97
|
-
}))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
104
|
/**
|
|
101
105
|
* Creates a new experiment.
|
|
102
106
|
* Cold method.
|
|
@@ -180,6 +184,11 @@ export class Abba {
|
|
|
180
184
|
await this.experimentDao.saveBatch(requiresUpdating, { saveMethod: 'update' })
|
|
181
185
|
}
|
|
182
186
|
|
|
187
|
+
async softDeleteExperiment(experimentId: string): Promise<void> {
|
|
188
|
+
await this.experimentDao.patchById(experimentId, { deleted: true, exclusions: [] })
|
|
189
|
+
await this.updateExclusions(experimentId, [])
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
/**
|
|
184
193
|
* Delete an experiment. Removes all user assignments and buckets.
|
|
185
194
|
* Requires the experiment to have been inactive for at least 15 minutes in order to
|
|
@@ -253,7 +262,7 @@ export class Abba {
|
|
|
253
262
|
return null
|
|
254
263
|
}
|
|
255
264
|
|
|
256
|
-
const experiments = await this.
|
|
265
|
+
const experiments = await this.getUserExperiments(userId)
|
|
257
266
|
const exclusionSet = getUserExclusionSet(experiments)
|
|
258
267
|
if (!canGenerateNewAssignments(experiment, exclusionSet)) {
|
|
259
268
|
return null
|
|
@@ -311,7 +320,7 @@ export class Abba {
|
|
|
311
320
|
segmentationData: SegmentationData,
|
|
312
321
|
existingOnly = false,
|
|
313
322
|
): Promise<DecoratedUserAssignment[]> {
|
|
314
|
-
const experiments = await this.
|
|
323
|
+
const experiments = await this.getUserExperiments(userId)
|
|
315
324
|
const exclusionSet = getUserExclusionSet(experiments)
|
|
316
325
|
|
|
317
326
|
// Shuffling means that randomisation occurs in the mutual exclusion
|
|
@@ -4,13 +4,15 @@ import type { IsoDate } from '@naturalcycles/js-lib'
|
|
|
4
4
|
import { localDate } from '@naturalcycles/js-lib'
|
|
5
5
|
import type { BaseExperiment, Experiment } from '../types.js'
|
|
6
6
|
|
|
7
|
-
type ExperimentDBM = BaseExperiment & {
|
|
8
|
-
rules: string | null
|
|
9
|
-
exclusions: string | null
|
|
10
|
-
data: string | null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
export class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
|
|
8
|
+
async getAllExperiments(opt?: GetAllExperimentsOpts): Promise<Experiment[]> {
|
|
9
|
+
if (!opt?.includeDeleted) {
|
|
10
|
+
return await this.getAll()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return await this.query().filterEq('deleted', false).runQuery()
|
|
14
|
+
}
|
|
15
|
+
|
|
14
16
|
async getByKey(key: string): Promise<Experiment | null> {
|
|
15
17
|
return await this.getOneBy('key', key)
|
|
16
18
|
}
|
|
@@ -58,3 +60,13 @@ function parseMySQLDate(date: string): IsoDate {
|
|
|
58
60
|
if (date instanceof Date) return localDate(date).toISODate()
|
|
59
61
|
return date as IsoDate
|
|
60
62
|
}
|
|
63
|
+
|
|
64
|
+
type ExperimentDBM = BaseExperiment & {
|
|
65
|
+
rules: string | null
|
|
66
|
+
exclusions: string | null
|
|
67
|
+
data: string | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GetAllExperimentsOpts {
|
|
71
|
+
includeDeleted?: boolean
|
|
72
|
+
}
|
package/src/migrations/init.sql
CHANGED
package/src/types.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type BaseExperiment = BaseDBEntity & {
|
|
|
31
31
|
* Date range end for the experiment assignments
|
|
32
32
|
*/
|
|
33
33
|
endDateExcl: IsoDate
|
|
34
|
+
/**
|
|
35
|
+
* Whether the experiment is flagged as deleted. This acts as a soft delete only.
|
|
36
|
+
*/
|
|
37
|
+
deleted: boolean
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export type Experiment = BaseExperiment & {
|