@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 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
- * Updates all user assignments with a given userId with the provided userId.
16
+ * Returns all experiments.
17
17
  */
18
- updateUserId(oldId: string, newId: string): Promise<void>;
18
+ getAllExperimentsWithBucketsNoCache(opts?: GetAllExperimentsOpts): Promise<ExperimentWithBuckets[]>;
19
+ getUserExperiments(userId: string): Promise<UserExperiment[]>;
19
20
  /**
20
- * Returns all experiments.
21
+ * Updates all user assignments with a given userId with the provided userId.
21
22
  */
22
- getAllExperimentsWithBucketsNoCache(): Promise<ExperimentWithBuckets[]>;
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
- async getAllExperimentsWithUserAssignments(userId) {
28
- const experiments = await this.getAllExperimentsWithBuckets();
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.getAllExperimentsWithUserAssignments(userId);
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.getAllExperimentsWithUserAssignments(userId);
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 declare class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {
10
- getByKey(key: string): Promise<Experiment | null>;
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
  }
@@ -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
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/abba",
3
3
  "type": "module",
4
- "version": "2.0.2",
4
+ "version": "2.1.0",
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
7
  "build": "dev-lib build",
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(): Promise<ExperimentWithBuckets[]> {
45
- return await this.getAllExperimentsWithBucketsNoCache()
44
+ async getAllExperimentsWithBuckets(
45
+ opts?: GetAllExperimentsOpts,
46
+ ): Promise<ExperimentWithBuckets[]> {
47
+ return await this.getAllExperimentsWithBucketsNoCache(opts)
46
48
  }
47
49
 
48
- async getAllExperimentsWithUserAssignments(userId: string): Promise<UserExperiment[]> {
49
- const experiments = await this.getAllExperimentsWithBuckets()
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.getAllExperimentsWithUserAssignments(userId)
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.getAllExperimentsWithUserAssignments(userId)
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
+ }
@@ -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/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 & {