@naturalcycles/abba 2.0.2 → 2.1.1

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: [] }, { saveMethod: 'update' });
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
@@ -131,12 +135,8 @@ export class Abba {
131
135
  const hasBeenInactiveFor15Mins = experiment.status === AssignmentStatus.Inactive &&
132
136
  localTime(experiment.updated).isOlderThan(15, 'minute');
133
137
  _assert(hasBeenInactiveFor15Mins, 'Experiment must be inactive for at least 15 minutes before deletion');
134
- const userAssignmentDeleteQuery = this.userAssignmentDao
135
- .query()
136
- .filterEq('experimentId', experimentId);
137
- await this.userAssignmentDao.deleteByQuery(userAssignmentDeleteQuery, { chunkSize: 50000 });
138
- const bucketDeleteQuery = this.bucketDao.query().filterEq('experimentId', experimentId);
139
- await this.bucketDao.deleteByQuery(bucketDeleteQuery);
138
+ await this.userAssignmentDao.deleteByExperimentId(experimentId);
139
+ await this.bucketDao.deleteByExperimentId(experimentId);
140
140
  await this.experimentDao.deleteById(experimentId);
141
141
  await this.updateExclusions(experimentId, []);
142
142
  }
@@ -172,7 +172,7 @@ export class Abba {
172
172
  if (existingOnly || experiment.status === AssignmentStatus.Paused) {
173
173
  return null;
174
174
  }
175
- const experiments = await this.getAllExperimentsWithUserAssignments(userId);
175
+ const experiments = await this.getUserExperiments(userId);
176
176
  const exclusionSet = getUserExclusionSet(experiments);
177
177
  if (!canGenerateNewAssignments(experiment, exclusionSet)) {
178
178
  return null;
@@ -219,7 +219,7 @@ export class Abba {
219
219
  * Hot method.
220
220
  */
221
221
  async generateUserAssignments(userId, segmentationData, existingOnly = false) {
222
- const experiments = await this.getAllExperimentsWithUserAssignments(userId);
222
+ const experiments = await this.getUserExperiments(userId);
223
223
  const exclusionSet = getUserExclusionSet(experiments);
224
224
  // Shuffling means that randomisation occurs in the mutual exclusion
225
225
  // 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
@@ -6,6 +6,7 @@ type BucketDBM = BaseBucket & {
6
6
  };
7
7
  export declare class BucketDao extends CommonDao<Bucket, BucketDBM> {
8
8
  getByExperimentId(experimentId: string): Promise<Bucket[]>;
9
+ deleteByExperimentId(experimentId: string): Promise<void>;
9
10
  }
10
11
  export declare function bucketDao(db: CommonDB): BucketDao;
11
12
  export {};
@@ -3,6 +3,9 @@ export class BucketDao extends CommonDao {
3
3
  async getByExperimentId(experimentId) {
4
4
  return await this.query().filterEq('experimentId', experimentId).runQuery();
5
5
  }
6
+ async deleteByExperimentId(experimentId) {
7
+ await this.query().filterEq('experimentId', experimentId).deleteByQuery();
8
+ }
6
9
  }
7
10
  export function bucketDao(db) {
8
11
  return new BucketDao({
@@ -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
  }
@@ -4,6 +4,7 @@ import type { UserAssignment } from '../types.js';
4
4
  export declare class UserAssignmentDao extends CommonDao<UserAssignment> {
5
5
  getUserAssignmentByExperimentId(userId: string, experimentId: string): Promise<UserAssignment | null>;
6
6
  getUserAssigmentsByExperimentIds(userId: string, experimentIds: string[]): Promise<UserAssignment[]>;
7
+ deleteByExperimentId(experimentId: string): Promise<void>;
7
8
  getCountByExperimentId(experimentId: string): Promise<number>;
8
9
  getCountByBucketId(bucketId: string): Promise<number>;
9
10
  }
@@ -9,6 +9,9 @@ export class UserAssignmentDao extends CommonDao {
9
9
  const query = this.query().filterEq('userId', userId).filterIn('experimentId', experimentIds);
10
10
  return await this.runQuery(query);
11
11
  }
12
+ async deleteByExperimentId(experimentId) {
13
+ await this.query().filterEq('experimentId', experimentId).deleteByQuery();
14
+ }
12
15
  async getCountByExperimentId(experimentId) {
13
16
  return await this.query().filterEq('experimentId', experimentId).runQueryCount();
14
17
  }
@@ -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.1",
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,15 @@ 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(
189
+ experimentId,
190
+ { deleted: true, exclusions: [] },
191
+ { saveMethod: 'update' },
192
+ )
193
+ await this.updateExclusions(experimentId, [])
194
+ }
195
+
183
196
  /**
184
197
  * Delete an experiment. Removes all user assignments and buckets.
185
198
  * Requires the experiment to have been inactive for at least 15 minutes in order to
@@ -197,14 +210,8 @@ export class Abba {
197
210
  'Experiment must be inactive for at least 15 minutes before deletion',
198
211
  )
199
212
 
200
- const userAssignmentDeleteQuery = this.userAssignmentDao
201
- .query()
202
- .filterEq('experimentId', experimentId)
203
- await this.userAssignmentDao.deleteByQuery(userAssignmentDeleteQuery, { chunkSize: 50000 })
204
-
205
- const bucketDeleteQuery = this.bucketDao.query().filterEq('experimentId', experimentId)
206
- await this.bucketDao.deleteByQuery(bucketDeleteQuery)
207
-
213
+ await this.userAssignmentDao.deleteByExperimentId(experimentId)
214
+ await this.bucketDao.deleteByExperimentId(experimentId)
208
215
  await this.experimentDao.deleteById(experimentId)
209
216
  await this.updateExclusions(experimentId, [])
210
217
  }
@@ -253,7 +260,7 @@ export class Abba {
253
260
  return null
254
261
  }
255
262
 
256
- const experiments = await this.getAllExperimentsWithUserAssignments(userId)
263
+ const experiments = await this.getUserExperiments(userId)
257
264
  const exclusionSet = getUserExclusionSet(experiments)
258
265
  if (!canGenerateNewAssignments(experiment, exclusionSet)) {
259
266
  return null
@@ -311,7 +318,7 @@ export class Abba {
311
318
  segmentationData: SegmentationData,
312
319
  existingOnly = false,
313
320
  ): Promise<DecoratedUserAssignment[]> {
314
- const experiments = await this.getAllExperimentsWithUserAssignments(userId)
321
+ const experiments = await this.getUserExperiments(userId)
315
322
  const exclusionSet = getUserExclusionSet(experiments)
316
323
 
317
324
  // Shuffling means that randomisation occurs in the mutual exclusion
@@ -10,6 +10,10 @@ export class BucketDao extends CommonDao<Bucket, BucketDBM> {
10
10
  async getByExperimentId(experimentId: string): Promise<Bucket[]> {
11
11
  return await this.query().filterEq('experimentId', experimentId).runQuery()
12
12
  }
13
+
14
+ async deleteByExperimentId(experimentId: string): Promise<void> {
15
+ await this.query().filterEq('experimentId', experimentId).deleteByQuery()
16
+ }
13
17
  }
14
18
 
15
19
  export function bucketDao(db: CommonDB): BucketDao {
@@ -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
+ }
@@ -20,6 +20,10 @@ export class UserAssignmentDao extends CommonDao<UserAssignment> {
20
20
  return await this.runQuery(query)
21
21
  }
22
22
 
23
+ async deleteByExperimentId(experimentId: string): Promise<void> {
24
+ await this.query().filterEq('experimentId', experimentId).deleteByQuery()
25
+ }
26
+
23
27
  async getCountByExperimentId(experimentId: string): Promise<number> {
24
28
  return await this.query().filterEq('experimentId', experimentId).runQueryCount()
25
29
  }
@@ -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 & {