@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 CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Unsaved } from '@naturalcycles/js-lib';
2
- import type { AbbaConfig, Bucket, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, GeneratedUserAssignment, SegmentationData } from './types.js';
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
- getAllExperiments(): Promise<ExperimentWithBuckets[]>;
14
+ getAllExperimentsWithBuckets(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
14
15
  /**
15
- * Updates all user assignments with a given userId with the provided userId.
16
+ * Returns all experiments.
16
17
  */
17
- updateUserId(oldId: string, newId: string): Promise<void>;
18
+ getAllExperimentsWithBucketsNoCache(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
19
+ getUserExperiments(userId: string): Promise<UserExperiment[]>;
18
20
  /**
19
- * Returns all experiments.
21
+ * Updates all user assignments with a given userId with the provided userId.
20
22
  */
21
- getAllExperimentsNoCache(): Promise<ExperimentWithBuckets[]>;
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<GeneratedUserAssignment>;
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
- getAllExistingGeneratedUserAssignments(userId: string): Promise<GeneratedUserAssignment[]>;
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<GeneratedUserAssignment[]>;
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 getAllExperiments() {
25
- return await this.getAllExperimentsNoCache();
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 getAllExperimentsNoCache() {
38
- const experiments = await this.experimentDao.getAll();
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.getOneBy('key', experimentKey);
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.getBy('experimentId', experiment.id);
141
- const existingAssignments = await this.userAssignmentDao.getBy('userId', userId);
142
- const existing = existingAssignments.find(a => a.experimentId === experiment.id);
143
- if (existing) {
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
- experiment,
147
- assignment: {
148
- ...existing,
149
- experimentKey: experiment.key,
150
- bucketKey: bucket?.key || null,
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.getAllExperiments();
163
- const exclusionSet = getUserExclusionSet(experiments, existingAssignments);
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
- experiment,
183
- assignment: {
184
- ...newAssignment,
185
- experimentKey: experiment.key,
186
- bucketKey: bucket?.key || null,
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 getAllExistingGeneratedUserAssignments(userId) {
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
- experiment,
204
- assignment: {
205
- ...assignment,
206
- experimentKey: experiment.key,
207
- bucketKey: bucket?.key || null,
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.getAllExperiments();
220
- const existingAssignments = await this.userAssignmentDao.getBy('userId', userId);
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 existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
230
- if (existing) {
231
- const bucket = experiment.buckets.find(b => b.id === existing.bucketId);
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
- experiment,
234
- assignment: {
235
- ...existing,
236
- experimentKey: experiment.key,
237
- bucketKey: bucket?.key || null,
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
- else if (!existingOnly && canGenerateNewAssignments(experiment, exclusionSet)) {
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
- .query()
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, "getAllExperiments", null);
290
+ ], Abba.prototype, "getAllExperimentsWithBuckets", null);
@@ -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 = Saved<BaseBucket> & {
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 const bucketDao: (db: CommonDB) => BucketDao;
10
+ export declare function bucketDao(db: CommonDB): BucketDao;
11
11
  export {};
@@ -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 const bucketDao = (db) => new BucketDao({
5
- db,
6
- table: 'Bucket',
7
- hooks: {
8
- beforeBMToDBM: bm => {
9
- return {
10
- ...bm,
11
- data: bm.data ? JSON.stringify(bm.data) : null,
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
- beforeDBMToBM: dbm => ({
15
- ...dbm,
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
- type ExperimentDBM = Saved<BaseExperiment> & {
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 declare class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
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 const userAssignmentDao: (db: CommonDB) => UserAssignmentDao;
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
- });
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS `Experiment` (
25
25
  `rules` JSON NULL,
26
26
  `exclusions` JSON NULL,
27
27
  `data` JSON NULL,
28
+ `deleted` BOOLEAN NOT NULL DEFAULT FALSE,
28
29
 
29
30
  PRIMARY KEY (`id`),
30
31
  UNIQUE INDEX `key_unique` (`key`)
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, Saved } from '@naturalcycles/js-lib';
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 = Saved<Experiment> & {
39
- buckets: Saved<Bucket>[];
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 interface GeneratedUserAssignment {
55
- assignment: (Saved<UserAssignment> & {
56
- experimentKey: string;
57
- bucketKey: string | null;
58
- bucketData: AnyObject | null;
59
- }) | null;
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
+ }