@naturalcycles/abba 1.7.0 → 1.9.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.
Files changed (58) hide show
  1. package/dist/abba.d.ts +45 -72
  2. package/dist/abba.js +119 -165
  3. package/dist/dao/bucket.dao.d.ts +5 -0
  4. package/dist/dao/bucket.dao.js +15 -0
  5. package/dist/dao/experiment.dao.d.ts +10 -0
  6. package/dist/dao/experiment.dao.js +19 -0
  7. package/dist/dao/userAssignment.dao.d.ts +5 -0
  8. package/dist/dao/userAssignment.dao.js +15 -0
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +1 -1
  11. package/dist/migrations/init.sql +47 -0
  12. package/dist/types.d.ts +30 -7
  13. package/dist/util.d.ts +5 -21
  14. package/dist/util.js +0 -16
  15. package/package.json +9 -9
  16. package/readme.md +14 -15
  17. package/src/abba.ts +160 -191
  18. package/src/dao/bucket.dao.ts +13 -0
  19. package/src/dao/experiment.dao.ts +22 -0
  20. package/src/dao/userAssignment.dao.ts +13 -0
  21. package/src/index.ts +0 -3
  22. package/src/migrations/init.sql +47 -0
  23. package/src/types.ts +41 -7
  24. package/src/util.ts +5 -21
  25. package/dist/prisma-output/index-browser.js +0 -141
  26. package/dist/prisma-output/index.d.ts +0 -5526
  27. package/dist/prisma-output/index.js +0 -217
  28. package/dist/prisma-output/libquery_engine-darwin-arm64.dylib.node +0 -0
  29. package/dist/prisma-output/libquery_engine-darwin.dylib.node +0 -0
  30. package/dist/prisma-output/libquery_engine-debian-openssl-1.1.x.so.node +0 -0
  31. package/dist/prisma-output/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  32. package/dist/prisma-output/runtime/esm/index-browser.mjs +0 -2370
  33. package/dist/prisma-output/runtime/esm/index.mjs +0 -40587
  34. package/dist/prisma-output/runtime/esm/proxy.mjs +0 -113
  35. package/dist/prisma-output/runtime/index-browser.d.ts +0 -269
  36. package/dist/prisma-output/runtime/index-browser.js +0 -2621
  37. package/dist/prisma-output/runtime/index.d.ts +0 -1384
  38. package/dist/prisma-output/runtime/index.js +0 -59183
  39. package/dist/prisma-output/runtime/proxy.d.ts +0 -1384
  40. package/dist/prisma-output/runtime/proxy.js +0 -13576
  41. package/dist/prisma-output/schema.prisma +0 -47
  42. package/src/prisma-output/index-browser.js +0 -141
  43. package/src/prisma-output/index.d.ts +0 -5526
  44. package/src/prisma-output/index.js +0 -217
  45. package/src/prisma-output/libquery_engine-darwin-arm64.dylib.node +0 -0
  46. package/src/prisma-output/libquery_engine-darwin.dylib.node +0 -0
  47. package/src/prisma-output/libquery_engine-debian-openssl-1.1.x.so.node +0 -0
  48. package/src/prisma-output/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  49. package/src/prisma-output/runtime/esm/index-browser.mjs +0 -2370
  50. package/src/prisma-output/runtime/esm/index.mjs +0 -40587
  51. package/src/prisma-output/runtime/esm/proxy.mjs +0 -113
  52. package/src/prisma-output/runtime/index-browser.d.ts +0 -269
  53. package/src/prisma-output/runtime/index-browser.js +0 -2621
  54. package/src/prisma-output/runtime/index.d.ts +0 -1384
  55. package/src/prisma-output/runtime/index.js +0 -59183
  56. package/src/prisma-output/runtime/proxy.d.ts +0 -1384
  57. package/src/prisma-output/runtime/proxy.js +0 -13576
  58. package/src/prisma-output/schema.prisma +0 -47
package/src/abba.ts CHANGED
@@ -1,300 +1,269 @@
1
- import { Bucket, Experiment, Prisma, PrismaClient, UserAssignment } from './prisma-output'
2
- import { AssignmentStatus } from './types'
3
- import { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
1
+ import { _assert, _AsyncMemo, pMap, Saved } from '@naturalcycles/js-lib'
2
+ import { LRUMemoCache } from '@naturalcycles/nodejs-lib'
4
3
  import {
4
+ AbbaConfig,
5
+ AssignmentStatus,
6
+ Bucket,
5
7
  BucketInput,
8
+ Experiment,
6
9
  ExperimentWithBuckets,
7
- ExperimentInput,
8
- SegmentationData,
9
- SegmentationRule,
10
- AssignmentStatistics,
11
- } from '.'
10
+ UserAssignment,
11
+ } from './types'
12
+ import { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
13
+ import { ExperimentDao, experimentDao } from './dao/experiment.dao'
14
+ import { UserAssignmentDao, userAssignmentDao } from './dao/userAssignment.dao'
15
+ import { BucketDao, bucketDao } from './dao/bucket.dao'
16
+ import { SegmentationData, AssignmentStatistics } from '.'
12
17
 
13
- // Note: Schema currently contains an output dir which generates all the files to the prisma dir
14
- // it would be tidier not to include it here when possible later on:
15
- // Explanation is here: https://github.com/prisma/prisma/issues/9435#issuecomment-960290681
18
+ const CACHE_TTL = 600_000 // 10 minutes
16
19
 
17
20
  export class Abba {
18
- private client: PrismaClient
19
-
20
- constructor(dbUrl?: string) {
21
- this.client = new PrismaClient({
22
- datasources: {
23
- db: {
24
- url: dbUrl,
25
- },
26
- },
27
- })
21
+ constructor(public cfg: AbbaConfig) {
22
+ const { db } = cfg
23
+ this.experimentDao = experimentDao(db)
24
+ this.bucketDao = bucketDao(db)
25
+ this.userAssignmentDao = userAssignmentDao(db)
28
26
  }
27
+
28
+ private experimentDao: ExperimentDao
29
+ private bucketDao: BucketDao
30
+ private userAssignmentDao: UserAssignmentDao
31
+
29
32
  /**
30
- * Returns all experiments
33
+ * Returns all (active and inactive) experiments.
31
34
  *
32
- * @returns
35
+ * Cold method, not cached.
33
36
  */
34
- async getAllExperiments(excludeInactive: boolean = false): Promise<ExperimentWithBuckets[]> {
35
- return await this.client.experiment.findMany({
36
- where: excludeInactive ? { NOT: { status: AssignmentStatus.Inactive } } : undefined,
37
- include: { buckets: true },
38
- })
37
+ async getAllExperiments(): Promise<ExperimentWithBuckets[]> {
38
+ const experiments = await this.experimentDao.query().runQuery()
39
+
40
+ const buckets = await this.bucketDao
41
+ .query()
42
+ .filter(
43
+ 'experimentId',
44
+ 'in',
45
+ experiments.map(e => e.id),
46
+ )
47
+ .runQuery()
48
+
49
+ return experiments.map(experiment => ({
50
+ ...experiment,
51
+ buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
52
+ }))
39
53
  }
40
54
 
41
55
  /**
42
- * Creates a new experiment
43
- *
44
- * @param experiment
45
- * @param buckets
46
- * @returns
56
+ * Returns only active experiments.
57
+ * Hot method.
58
+ * Cached in-memory for N minutes (currently 10).
59
+ */
60
+ @_AsyncMemo({ cacheFactory: () => new LRUMemoCache({ ttl: CACHE_TTL, max: 1 }) })
61
+ async getActiveExperiments(): Promise<ExperimentWithBuckets[]> {
62
+ const experiments = await this.experimentDao
63
+ .query()
64
+ .filter('status', '!=', AssignmentStatus.Inactive)
65
+ .runQuery()
66
+
67
+ const buckets = await this.bucketDao
68
+ .query()
69
+ .filter(
70
+ 'experimentId',
71
+ 'in',
72
+ experiments.map(e => e.id),
73
+ )
74
+ .runQuery()
75
+
76
+ return experiments.map(experiment => ({
77
+ ...experiment,
78
+ buckets: buckets.filter(bucket => bucket.experimentId === experiment.id),
79
+ }))
80
+ }
81
+
82
+ /**
83
+ * Creates a new experiment.
84
+ * Cold method.
47
85
  */
48
86
  async createExperiment(
49
- experiment: ExperimentInput,
87
+ experiment: Experiment,
50
88
  buckets: BucketInput[],
51
89
  ): Promise<ExperimentWithBuckets> {
52
90
  if (experiment.status === AssignmentStatus.Active) {
53
91
  validateTotalBucketRatio(buckets)
54
92
  }
55
93
 
56
- const created = await this.client.experiment.create({
57
- data: {
58
- ...experiment,
59
- rules: experiment.rules as Prisma.InputJsonArray,
60
- buckets: {
61
- createMany: {
62
- data: buckets,
63
- },
64
- },
65
- },
66
- include: {
67
- buckets: true,
68
- },
69
- })
70
- return created
94
+ await this.experimentDao.save(experiment)
95
+
96
+ return {
97
+ ...(experiment as Saved<Experiment>),
98
+ buckets: await this.bucketDao.saveBatch(
99
+ buckets.map(b => ({ ...b, experimentId: experiment.id! })),
100
+ ),
101
+ }
71
102
  }
72
103
 
73
104
  /**
74
- * Update experiment information, will also validate the buckets ratio if experiment.active is true
75
- *
76
- * @param id
77
- * @param experiment
78
- * @param rules
79
- * @param buckets
80
- * @returns
105
+ * Update experiment information, will also validate the buckets' ratio if experiment.active is true
106
+ * Cold method.
81
107
  */
82
108
  async saveExperiment(
83
- id: number,
84
- experiment: ExperimentInput,
85
- buckets: BucketInput[],
109
+ experiment: Experiment & { id: number },
110
+ buckets: Bucket[],
86
111
  ): Promise<ExperimentWithBuckets> {
87
112
  if (experiment.status === AssignmentStatus.Active) {
88
113
  validateTotalBucketRatio(buckets)
89
114
  }
90
115
 
91
- const updatedExperiment = await this.updateExperiment(id, experiment)
92
- const updatedBuckets = await this.saveBuckets(buckets)
116
+ await this.experimentDao.save(experiment)
93
117
 
94
118
  return {
95
- ...updatedExperiment,
96
- buckets: updatedBuckets,
119
+ ...(experiment as Saved<Experiment>),
120
+ buckets: await this.bucketDao.saveBatch(
121
+ buckets.map(b => ({ ...b, experimentId: experiment.id })),
122
+ ),
97
123
  }
98
124
  }
99
125
 
100
126
  /**
101
- * Delete an experiment. Removes all user assignments and buckets
102
- *
103
- * @param id
127
+ * Delete an experiment. Removes all user assignments and buckets.
128
+ * Cold method.
104
129
  */
105
130
  async deleteExperiment(id: number): Promise<void> {
106
- await this.client.experiment.delete({ where: { id } })
131
+ await this.experimentDao.deleteById(id)
107
132
  }
108
133
 
109
134
  /**
110
- * Get an assignment for a given user. If existingOnly is false, it will attempt generate a new assignment
135
+ * Get an assignment for a given user. If existingOnly is false, it will attempt to generate a new assignment
136
+ * Cold method.
111
137
  *
112
138
  * @param experimentId
113
139
  * @param userId
114
- * @param createNew
115
- * @param segmentationData
116
- * @returns
140
+ * @param existingOnly Do not generate any new assignments for this experiment
141
+ * @param segmentationData Required if existingOnly is false
117
142
  */
118
143
  async getUserAssignment(
119
144
  experimentId: number,
120
145
  userId: string,
121
146
  existingOnly: boolean,
122
147
  segmentationData?: SegmentationData,
123
- ): Promise<UserAssignment | null> {
124
- const experiment = await this.client.experiment.findUnique({
125
- where: { id: experimentId },
126
- include: { buckets: true },
127
- })
128
- if (!experiment) throw new Error('Experiment not found')
129
-
148
+ ): Promise<Saved<UserAssignment> | null> {
130
149
  const existing = await this.getExistingUserAssignment(experimentId, userId)
131
150
  if (existing) return existing
151
+ if (existingOnly) return null
132
152
 
133
- if (experiment.status !== AssignmentStatus.Active || existingOnly) return null
153
+ const experiment = await this.experimentDao.requireById(experimentId)
154
+ if (experiment.status !== AssignmentStatus.Active) return null
134
155
 
135
- if (!segmentationData)
136
- throw new Error('Segmentation data required when creating a new assignment')
156
+ _assert(segmentationData, 'Segmentation data required when creating a new assignment')
137
157
 
138
- return await this.generateUserAssignment(experiment, userId, segmentationData)
158
+ const buckets = await this.bucketDao.getBy('experimentId', experimentId)
159
+
160
+ const assignment = this.generateUserAssignmentData(
161
+ { ...experiment, buckets },
162
+ userId,
163
+ segmentationData,
164
+ )
165
+ if (!assignment) return null
166
+
167
+ return await this.userAssignmentDao.save(assignment)
139
168
  }
140
169
 
141
170
  /**
142
- * Get all existing user assignments
143
- *
144
- * @param userId G
145
- * @returns
171
+ * Get all existing user assignments.
172
+ * Hot method.
173
+ * Not cached, because Assignments are fast-changing.
146
174
  */
147
- async getAllExistingUserAssignments(userId: string): Promise<UserAssignment[]> {
148
- return await this.client.userAssignment.findMany({ where: { userId } })
175
+ async getAllExistingUserAssignments(userId: string): Promise<Saved<UserAssignment>[]> {
176
+ return await this.userAssignmentDao.getBy('userId', userId)
149
177
  }
150
178
 
151
179
  /**
152
- * Generate user assignments for all active experiments. Will return any existing and attempt to generate any new assignments.
153
- *
154
- * @param userId
155
- * @param segmentationData
156
- * @returns
180
+ * Generate user assignments for all active experiments.
181
+ * Will return any existing and attempt to generate any new assignments.
182
+ * Hot method.
157
183
  */
158
184
  async generateUserAssignments(
159
185
  userId: string,
160
186
  segmentationData: SegmentationData,
161
- ): Promise<UserAssignment[]> {
162
- const experiments = await this.getAllExperiments(true)
187
+ ): Promise<Saved<UserAssignment>[]> {
188
+ const experiments = await this.getActiveExperiments() // cached
163
189
  const existingAssignments = await this.getAllExistingUserAssignments(userId)
164
190
 
165
- const assignments: UserAssignment[] = []
166
- const generatedAssignments: Promise<UserAssignment | null>[] = []
191
+ const allAssignments: Saved<UserAssignment>[] = []
192
+ const generatedAssignments: UserAssignment[] = []
167
193
 
168
194
  for (const experiment of experiments) {
169
195
  const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
170
196
  if (existing) {
171
- assignments.push(existing)
172
- continue
197
+ allAssignments.push(existing)
173
198
  } else {
174
- generatedAssignments.push(this.generateUserAssignment(experiment, userId, segmentationData))
199
+ const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData)
200
+ if (assignment) {
201
+ generatedAssignments.push(assignment)
202
+ }
175
203
  }
176
204
  }
177
205
 
178
- const generated = await Promise.all(generatedAssignments)
179
- const filtered = generated.filter((ua): ua is UserAssignment => ua !== null)
180
- return [...assignments, ...filtered]
206
+ await this.userAssignmentDao.saveBatch(generatedAssignments)
207
+
208
+ return [...allAssignments, ...(generatedAssignments as Saved<UserAssignment>[])]
181
209
  }
182
210
 
183
211
  /**
184
- * Get assignment statistics for an experiment
185
- *
186
- * @param experimentId
187
- * @returns
212
+ * Get assignment statistics for an experiment.
213
+ * Cold method.
188
214
  */
189
215
  async getExperimentAssignmentStatistics(experimentId: number): Promise<AssignmentStatistics> {
190
216
  const statistics = {
191
- sampled: await this.client.userAssignment.count({ where: { experimentId } }),
217
+ sampled: await this.userAssignmentDao
218
+ .query()
219
+ .filterEq('experimentId', experimentId)
220
+ .runQueryCount(),
192
221
  buckets: {},
193
222
  }
194
223
 
195
- const buckets = await this.client.bucket.findMany({ where: { experimentId } })
196
- const assignmentCounts = await this.client.userAssignment.groupBy({
197
- where: { experimentId },
198
- by: ['bucketId'],
199
- _count: {
200
- _all: true,
201
- },
202
- })
203
-
204
- buckets.forEach(({ id }) => {
205
- statistics.buckets[`${id}`] = assignmentCounts.find(i => i.bucketId === id)?._count?._all || 0
224
+ const buckets = await this.bucketDao.getBy('experimentId', experimentId)
225
+ await pMap(buckets, async bucket => {
226
+ statistics[bucket.id] = await this.userAssignmentDao
227
+ .query()
228
+ .filterEq('bucketId', bucket.id)
229
+ .runQueryCount()
206
230
  })
207
231
 
208
232
  return statistics
209
233
  }
210
234
 
211
235
  /**
212
- * Generate a new assignment for a given user
213
- *
214
- * @param experimentId
215
- * @param userId
216
- * @param segmentationData
217
- * @returns
236
+ * Generate a new assignment for a given user.
237
+ * Doesn't save it.
218
238
  */
219
- private async generateUserAssignment(
239
+ private generateUserAssignmentData(
220
240
  experiment: ExperimentWithBuckets,
221
241
  userId: string,
222
242
  segmentationData: SegmentationData,
223
- ): Promise<UserAssignment | null> {
224
- const segmentationMatch = validateSegmentationRules(
225
- experiment.rules as unknown as SegmentationRule[],
226
- segmentationData,
227
- )
243
+ ): UserAssignment | null {
244
+ const segmentationMatch = validateSegmentationRules(experiment.rules, segmentationData)
228
245
  if (!segmentationMatch) return null
229
246
 
230
- const bucketId = determineAssignment(experiment.sampling, experiment.buckets)
231
- return await this.client.userAssignment.create({
232
- data: { userId, experimentId: experiment.id, bucketId },
233
- })
247
+ return {
248
+ userId,
249
+ experimentId: experiment.id,
250
+ bucketId: determineAssignment(experiment.sampling, experiment.buckets),
251
+ }
234
252
  }
235
253
 
236
254
  /**
237
255
  * Queries to retrieve an existing user assignment for a given experiment
238
- *
239
- * @param experimentId
240
- * @param userId
241
- * @returns
242
256
  */
243
257
  private async getExistingUserAssignment(
244
258
  experimentId: number,
245
259
  userId: string,
246
- ): Promise<UserAssignment | null> {
247
- return await this.client.userAssignment.findFirst({ where: { userId, experimentId } })
248
- }
249
-
250
- /**
251
- * Update experiment information
252
- *
253
- * @param id
254
- * @param experiment
255
- * @param rules
256
- * @returns
257
- */
258
- private async updateExperiment(id: number, experiment: Partial<Experiment>): Promise<Experiment> {
259
- return await this.client.experiment.update({
260
- where: { id },
261
- data: {
262
- ...experiment,
263
- rules: experiment.rules as Prisma.InputJsonArray,
264
- },
265
- })
266
- }
267
-
268
- /**
269
- * Upserts bucket info
270
- *
271
- * @param id
272
- * @param experiment
273
- * @returns
274
- */
275
- private async saveBuckets(buckets: BucketInput[]): Promise<Bucket[]> {
276
- const savedBuckets: Promise<Bucket>[] = []
277
- for (const bucket of buckets) {
278
- const { id, ...data } = bucket
279
- if (id) {
280
- savedBuckets.push(this.client.bucket.update({ where: { id }, data }))
281
- } else {
282
- savedBuckets.push(
283
- this.client.bucket.create({
284
- data: {
285
- key: data.key,
286
- ratio: data.ratio,
287
- experiment: {
288
- connect: {
289
- id: data.experimentId,
290
- },
291
- },
292
- },
293
- }),
294
- )
295
- }
296
- }
260
+ ): Promise<Saved<UserAssignment> | null> {
261
+ const [assignment] = await this.userAssignmentDao
262
+ .query()
263
+ .filterEq('userId', userId)
264
+ .filterEq('experimentId', experimentId)
265
+ .runQuery()
297
266
 
298
- return await Promise.all(savedBuckets)
267
+ return assignment || null
299
268
  }
300
269
  }
@@ -0,0 +1,13 @@
1
+ import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
+ import { Bucket } from '../types'
3
+
4
+ export class BucketDao extends CommonDao<Bucket> {}
5
+
6
+ export const bucketDao = (db: CommonDB): BucketDao =>
7
+ new BucketDao({
8
+ db,
9
+ table: 'Bucket',
10
+ createId: false, // mysql auto_increment is used instead
11
+ idType: 'number',
12
+ assignGeneratedIds: true,
13
+ })
@@ -0,0 +1,22 @@
1
+ import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
+ import { Saved } from '@naturalcycles/js-lib'
3
+ import { BaseExperiment, Experiment } from '../types'
4
+
5
+ type ExperimentDBM = Saved<BaseExperiment> & {
6
+ rules: string | null
7
+ }
8
+
9
+ export class ExperimentDao extends CommonDao<Experiment, ExperimentDBM> {}
10
+
11
+ export const experimentDao = (db: CommonDB): ExperimentDao =>
12
+ new ExperimentDao({
13
+ db,
14
+ table: 'Experiment',
15
+ createId: false, // mysql auto_increment is used instead
16
+ idType: 'number',
17
+ assignGeneratedIds: true,
18
+ hooks: {
19
+ beforeBMToDBM: bm => ({ ...bm, rules: bm.rules.length ? JSON.stringify(bm.rules) : null }),
20
+ beforeDBMToBM: dbm => ({ ...dbm, rules: (dbm.rules && JSON.parse(dbm.rules)) || [] }),
21
+ },
22
+ })
@@ -0,0 +1,13 @@
1
+ import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
+ import { UserAssignment } from '../types'
3
+
4
+ export class UserAssignmentDao extends CommonDao<UserAssignment> {}
5
+
6
+ export const userAssignmentDao = (db: CommonDB): UserAssignmentDao =>
7
+ new UserAssignmentDao({
8
+ db,
9
+ table: 'UserAssignment',
10
+ createId: false, // mysql auto_increment is used instead
11
+ idType: 'number',
12
+ assignGeneratedIds: true,
13
+ })
package/src/index.ts CHANGED
@@ -1,5 +1,2 @@
1
- export type { Bucket, Experiment, UserAssignment } from './prisma-output'
2
-
3
1
  export * from './types'
4
-
5
2
  export { Abba } from './abba'
@@ -0,0 +1,47 @@
1
+ -- CreateTable
2
+ CREATE TABLE IF NOT EXISTS `Bucket` (
3
+ `id` INTEGER NOT NULL AUTO_INCREMENT,
4
+ `experimentId` INTEGER NOT NULL,
5
+ `key` VARCHAR(10) NOT NULL,
6
+ `ratio` INTEGER NOT NULL,
7
+ `created` INTEGER(11) NOT NULL,
8
+ `updated` INTEGER(11) NOT NULL,
9
+
10
+ PRIMARY KEY (`id`)
11
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
12
+
13
+ -- CreateTable
14
+ CREATE TABLE IF NOT EXISTS `Experiment` (
15
+ `id` INTEGER NOT NULL AUTO_INCREMENT,
16
+ `name` VARCHAR(191) NOT NULL,
17
+ `status` INTEGER NOT NULL,
18
+ `sampling` INTEGER NOT NULL,
19
+ `description` VARCHAR(240) NULL,
20
+ `created` INTEGER(11) NOT NULL,
21
+ `updated` INTEGER(11) NOT NULL,
22
+ `rules` JSON NULL,
23
+
24
+ PRIMARY KEY (`id`)
25
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
26
+
27
+ -- CreateTable
28
+ CREATE TABLE IF NOT EXISTS `UserAssignment` (
29
+ `id` INTEGER NOT NULL AUTO_INCREMENT,
30
+ `userId` VARCHAR(191) NOT NULL,
31
+ `experimentId` INTEGER NOT NULL,
32
+ `bucketId` INTEGER NULL,
33
+ `created` INTEGER(11) NOT NULL,
34
+ `updated` INTEGER(11) NOT NULL,
35
+
36
+ UNIQUE INDEX `UserAssignment_userId_experimentId_key`(`userId`, `experimentId`),
37
+ PRIMARY KEY (`id`)
38
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
39
+
40
+ -- AddForeignKey
41
+ ALTER TABLE `Bucket` ADD CONSTRAINT `Bucket_experimentId_fkey` FOREIGN KEY (`experimentId`) REFERENCES `Experiment`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
42
+
43
+ -- AddForeignKey
44
+ ALTER TABLE `UserAssignment` ADD CONSTRAINT `UserAssignment_bucketId_fkey` FOREIGN KEY (`bucketId`) REFERENCES `Bucket`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
45
+
46
+ -- AddForeignKey
47
+ ALTER TABLE `UserAssignment` ADD CONSTRAINT `UserAssignment_experimentId_fkey` FOREIGN KEY (`experimentId`) REFERENCES `Experiment`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
package/src/types.ts CHANGED
@@ -1,16 +1,50 @@
1
- import { Bucket, Experiment } from './prisma-output'
1
+ import { CommonDB } from '@naturalcycles/db-lib'
2
+ import { BaseDBEntity, Saved } from '@naturalcycles/js-lib'
2
3
 
3
- export type Unsaved<T> = Omit<T, 'createdAt' | 'updatedAt'> & {
4
- id?: number
4
+ export interface AbbaConfig {
5
+ db: CommonDB
6
+
7
+ /**
8
+ * Determines for how long to cache "hot" requests.
9
+ * Caches in memory.
10
+ * Default is 10.
11
+ * Set to 0 to disable cache (every request will hit the DB).
12
+ */
13
+ // cacheMinutes?: number
14
+ }
15
+
16
+ export type BaseExperiment = BaseDBEntity<number> & {
17
+ name: string
18
+ status: number
19
+ sampling: number
20
+ description: string | null
5
21
  }
6
22
 
7
- export type ExperimentWithBuckets = Experiment & {
8
- buckets: Bucket[]
23
+ export type Experiment = BaseExperiment & {
24
+ rules: SegmentationRule[]
9
25
  }
10
26
 
11
- export type ExperimentInput = Unsaved<Experiment>
27
+ export type ExperimentWithBuckets = Saved<Experiment> & {
28
+ buckets: Saved<Bucket>[]
29
+ }
12
30
 
13
- export type BucketInput = Unsaved<Bucket>
31
+ export interface BucketInput {
32
+ experimentId: number
33
+ key: string
34
+ ratio: number
35
+ }
36
+
37
+ export type Bucket = BaseDBEntity<number> & {
38
+ experimentId: number
39
+ key: string
40
+ ratio: number
41
+ }
42
+
43
+ export type UserAssignment = BaseDBEntity<number> & {
44
+ userId: string
45
+ experimentId: number
46
+ bucketId: number | null
47
+ }
14
48
 
15
49
  export type SegmentationData = Record<string, string | boolean | number>
16
50