@naturalcycles/abba 1.7.0 → 1.8.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 +15 -29
- package/dist/abba.js +56 -107
- package/dist/dao/bucket.dao.d.ts +5 -0
- package/dist/dao/bucket.dao.js +15 -0
- package/dist/dao/experiment.dao.d.ts +10 -0
- package/dist/dao/experiment.dao.js +19 -0
- package/dist/dao/userAssignment.dao.d.ts +5 -0
- package/dist/dao/userAssignment.dao.js +15 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/migrations/init.sql +47 -0
- package/dist/types.d.ts +30 -7
- package/dist/util.d.ts +5 -5
- package/package.json +7 -8
- package/readme.md +8 -9
- package/src/abba.ts +90 -134
- package/src/dao/bucket.dao.ts +13 -0
- package/src/dao/experiment.dao.ts +22 -0
- package/src/dao/userAssignment.dao.ts +13 -0
- package/src/index.ts +0 -3
- package/src/migrations/init.sql +47 -0
- package/src/types.ts +33 -7
- package/src/util.ts +5 -5
- package/dist/prisma-output/index-browser.js +0 -141
- package/dist/prisma-output/index.d.ts +0 -5526
- package/dist/prisma-output/index.js +0 -217
- package/dist/prisma-output/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/dist/prisma-output/libquery_engine-darwin.dylib.node +0 -0
- package/dist/prisma-output/libquery_engine-debian-openssl-1.1.x.so.node +0 -0
- package/dist/prisma-output/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/dist/prisma-output/runtime/esm/index-browser.mjs +0 -2370
- package/dist/prisma-output/runtime/esm/index.mjs +0 -40587
- package/dist/prisma-output/runtime/esm/proxy.mjs +0 -113
- package/dist/prisma-output/runtime/index-browser.d.ts +0 -269
- package/dist/prisma-output/runtime/index-browser.js +0 -2621
- package/dist/prisma-output/runtime/index.d.ts +0 -1384
- package/dist/prisma-output/runtime/index.js +0 -59183
- package/dist/prisma-output/runtime/proxy.d.ts +0 -1384
- package/dist/prisma-output/runtime/proxy.js +0 -13576
- package/dist/prisma-output/schema.prisma +0 -47
- package/src/prisma-output/index-browser.js +0 -141
- package/src/prisma-output/index.d.ts +0 -5526
- package/src/prisma-output/index.js +0 -217
- package/src/prisma-output/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/src/prisma-output/libquery_engine-darwin.dylib.node +0 -0
- package/src/prisma-output/libquery_engine-debian-openssl-1.1.x.so.node +0 -0
- package/src/prisma-output/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/src/prisma-output/runtime/esm/index-browser.mjs +0 -2370
- package/src/prisma-output/runtime/esm/index.mjs +0 -40587
- package/src/prisma-output/runtime/esm/proxy.mjs +0 -113
- package/src/prisma-output/runtime/index-browser.d.ts +0 -269
- package/src/prisma-output/runtime/index-browser.js +0 -2621
- package/src/prisma-output/runtime/index.d.ts +0 -1384
- package/src/prisma-output/runtime/index.js +0 -59183
- package/src/prisma-output/runtime/proxy.d.ts +0 -1384
- package/src/prisma-output/runtime/proxy.js +0 -13576
- package/src/prisma-output/schema.prisma +0 -47
package/src/abba.ts
CHANGED
|
@@ -1,41 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AssignmentStatus } from './types'
|
|
3
|
-
import { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
|
|
1
|
+
import { pMap, Saved } from '@naturalcycles/js-lib'
|
|
4
2
|
import {
|
|
5
|
-
|
|
3
|
+
AbbaConfig,
|
|
4
|
+
AssignmentStatus,
|
|
5
|
+
Bucket,
|
|
6
|
+
Experiment,
|
|
6
7
|
ExperimentWithBuckets,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '.'
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
8
|
+
UserAssignment,
|
|
9
|
+
} from './types'
|
|
10
|
+
import { determineAssignment, validateSegmentationRules, validateTotalBucketRatio } from './util'
|
|
11
|
+
import { ExperimentDao, experimentDao } from './dao/experiment.dao'
|
|
12
|
+
import { UserAssignmentDao, userAssignmentDao } from './dao/userAssignment.dao'
|
|
13
|
+
import { BucketDao, bucketDao } from './dao/bucket.dao'
|
|
14
|
+
import { SegmentationData, SegmentationRule, AssignmentStatistics } from '.'
|
|
16
15
|
|
|
17
16
|
export class Abba {
|
|
18
|
-
private
|
|
17
|
+
private experimentDao: ExperimentDao
|
|
18
|
+
private bucketDao: BucketDao
|
|
19
|
+
private userAssignmentDao: UserAssignmentDao
|
|
19
20
|
|
|
20
|
-
constructor(
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
url: dbUrl,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
})
|
|
21
|
+
constructor({ db }: AbbaConfig) {
|
|
22
|
+
this.experimentDao = experimentDao(db)
|
|
23
|
+
this.bucketDao = bucketDao(db)
|
|
24
|
+
this.userAssignmentDao = userAssignmentDao(db)
|
|
28
25
|
}
|
|
26
|
+
|
|
27
|
+
// TODO: Cache me
|
|
29
28
|
/**
|
|
30
29
|
* Returns all experiments
|
|
31
30
|
*
|
|
32
31
|
* @returns
|
|
33
32
|
*/
|
|
34
33
|
async getAllExperiments(excludeInactive: boolean = false): Promise<ExperimentWithBuckets[]> {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
34
|
+
const query = this.experimentDao.query()
|
|
35
|
+
if (excludeInactive) {
|
|
36
|
+
query.filter('status', '!=', AssignmentStatus.Inactive)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const experiments = await this.experimentDao.runQuery(query)
|
|
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
|
/**
|
|
@@ -46,28 +60,21 @@ export class Abba {
|
|
|
46
60
|
* @returns
|
|
47
61
|
*/
|
|
48
62
|
async createExperiment(
|
|
49
|
-
experiment:
|
|
50
|
-
buckets:
|
|
63
|
+
experiment: Experiment,
|
|
64
|
+
buckets: Bucket[],
|
|
51
65
|
): Promise<ExperimentWithBuckets> {
|
|
52
66
|
if (experiment.status === AssignmentStatus.Active) {
|
|
53
67
|
validateTotalBucketRatio(buckets)
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
const created = await this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
include: {
|
|
67
|
-
buckets: true,
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
return created
|
|
70
|
+
const created = await this.experimentDao.save(experiment)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...created,
|
|
74
|
+
buckets: await this.bucketDao.saveBatch(
|
|
75
|
+
buckets.map(b => ({ ...b, experimentId: created.id })),
|
|
76
|
+
),
|
|
77
|
+
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
/**
|
|
@@ -81,19 +88,21 @@ export class Abba {
|
|
|
81
88
|
*/
|
|
82
89
|
async saveExperiment(
|
|
83
90
|
id: number,
|
|
84
|
-
experiment:
|
|
85
|
-
buckets:
|
|
91
|
+
experiment: Experiment,
|
|
92
|
+
buckets: Bucket[],
|
|
86
93
|
): Promise<ExperimentWithBuckets> {
|
|
87
94
|
if (experiment.status === AssignmentStatus.Active) {
|
|
88
95
|
validateTotalBucketRatio(buckets)
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
const
|
|
92
|
-
|
|
98
|
+
const updated = await this.experimentDao.save({
|
|
99
|
+
...experiment,
|
|
100
|
+
id,
|
|
101
|
+
})
|
|
93
102
|
|
|
94
103
|
return {
|
|
95
|
-
...
|
|
96
|
-
buckets:
|
|
104
|
+
...updated,
|
|
105
|
+
buckets: await this.bucketDao.saveBatch(buckets.map(b => ({ ...b, experimentId: id }))),
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
|
|
@@ -103,7 +112,7 @@ export class Abba {
|
|
|
103
112
|
* @param id
|
|
104
113
|
*/
|
|
105
114
|
async deleteExperiment(id: number): Promise<void> {
|
|
106
|
-
await this.
|
|
115
|
+
await this.experimentDao.deleteById(id)
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/**
|
|
@@ -111,8 +120,8 @@ export class Abba {
|
|
|
111
120
|
*
|
|
112
121
|
* @param experimentId
|
|
113
122
|
* @param userId
|
|
114
|
-
* @param
|
|
115
|
-
* @param segmentationData
|
|
123
|
+
* @param existingOnly Do not generate any new assignments for this experiment
|
|
124
|
+
* @param segmentationData Required if existingOnly is false
|
|
116
125
|
* @returns
|
|
117
126
|
*/
|
|
118
127
|
async getUserAssignment(
|
|
@@ -120,13 +129,12 @@ export class Abba {
|
|
|
120
129
|
userId: string,
|
|
121
130
|
existingOnly: boolean,
|
|
122
131
|
segmentationData?: SegmentationData,
|
|
123
|
-
): Promise<UserAssignment | null> {
|
|
124
|
-
const experiment = await this.
|
|
125
|
-
where: { id: experimentId },
|
|
126
|
-
include: { buckets: true },
|
|
127
|
-
})
|
|
132
|
+
): Promise<Saved<UserAssignment> | null> {
|
|
133
|
+
const experiment = await this.experimentDao.requireById(experimentId)
|
|
128
134
|
if (!experiment) throw new Error('Experiment not found')
|
|
129
135
|
|
|
136
|
+
const buckets = await this.bucketDao.getBy('experimentId', experimentId)
|
|
137
|
+
|
|
130
138
|
const existing = await this.getExistingUserAssignment(experimentId, userId)
|
|
131
139
|
if (existing) return existing
|
|
132
140
|
|
|
@@ -135,17 +143,17 @@ export class Abba {
|
|
|
135
143
|
if (!segmentationData)
|
|
136
144
|
throw new Error('Segmentation data required when creating a new assignment')
|
|
137
145
|
|
|
138
|
-
return await this.generateUserAssignment(experiment, userId, segmentationData)
|
|
146
|
+
return await this.generateUserAssignment({ ...experiment, buckets }, userId, segmentationData)
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
/**
|
|
142
150
|
* Get all existing user assignments
|
|
143
151
|
*
|
|
144
|
-
* @param userId
|
|
152
|
+
* @param userId
|
|
145
153
|
* @returns
|
|
146
154
|
*/
|
|
147
|
-
async getAllExistingUserAssignments(userId: string): Promise<UserAssignment[]> {
|
|
148
|
-
return await this.
|
|
155
|
+
async getAllExistingUserAssignments(userId: string): Promise<Saved<UserAssignment>[]> {
|
|
156
|
+
return await this.userAssignmentDao.getBy('userId', userId)
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/**
|
|
@@ -158,12 +166,12 @@ export class Abba {
|
|
|
158
166
|
async generateUserAssignments(
|
|
159
167
|
userId: string,
|
|
160
168
|
segmentationData: SegmentationData,
|
|
161
|
-
): Promise<UserAssignment[]> {
|
|
169
|
+
): Promise<Saved<UserAssignment>[]> {
|
|
162
170
|
const experiments = await this.getAllExperiments(true)
|
|
163
171
|
const existingAssignments = await this.getAllExistingUserAssignments(userId)
|
|
164
172
|
|
|
165
|
-
const assignments: UserAssignment[] = []
|
|
166
|
-
const generatedAssignments: Promise<UserAssignment | null>[] = []
|
|
173
|
+
const assignments: Saved<UserAssignment>[] = []
|
|
174
|
+
const generatedAssignments: Promise<Saved<UserAssignment> | null>[] = []
|
|
167
175
|
|
|
168
176
|
for (const experiment of experiments) {
|
|
169
177
|
const existing = existingAssignments.find(ua => ua.experimentId === experiment.id)
|
|
@@ -176,7 +184,7 @@ export class Abba {
|
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
const generated = await Promise.all(generatedAssignments)
|
|
179
|
-
const filtered = generated.filter((ua): ua is UserAssignment => ua !== null)
|
|
187
|
+
const filtered = generated.filter((ua): ua is Saved<UserAssignment> => ua !== null)
|
|
180
188
|
return [...assignments, ...filtered]
|
|
181
189
|
}
|
|
182
190
|
|
|
@@ -188,21 +196,17 @@ export class Abba {
|
|
|
188
196
|
*/
|
|
189
197
|
async getExperimentAssignmentStatistics(experimentId: number): Promise<AssignmentStatistics> {
|
|
190
198
|
const statistics = {
|
|
191
|
-
sampled: await this.
|
|
199
|
+
sampled: await this.userAssignmentDao
|
|
200
|
+
.query()
|
|
201
|
+
.filterEq('experimentId', experimentId)
|
|
202
|
+
.runQueryCount(),
|
|
192
203
|
buckets: {},
|
|
193
204
|
}
|
|
194
205
|
|
|
195
|
-
const buckets = await this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
206
|
+
const buckets = await this.bucketDao.getBy('experimentId', experimentId)
|
|
207
|
+
await pMap(buckets, async bucket => {
|
|
208
|
+
const count = this.userAssignmentDao.query().filterEq('bucketId', bucket.id)
|
|
209
|
+
statistics[bucket.id] = await this.userAssignmentDao.runQueryCount(count)
|
|
206
210
|
})
|
|
207
211
|
|
|
208
212
|
return statistics
|
|
@@ -220,16 +224,17 @@ export class Abba {
|
|
|
220
224
|
experiment: ExperimentWithBuckets,
|
|
221
225
|
userId: string,
|
|
222
226
|
segmentationData: SegmentationData,
|
|
223
|
-
): Promise<UserAssignment | null> {
|
|
227
|
+
): Promise<Saved<UserAssignment> | null> {
|
|
224
228
|
const segmentationMatch = validateSegmentationRules(
|
|
225
229
|
experiment.rules as unknown as SegmentationRule[],
|
|
226
230
|
segmentationData,
|
|
227
231
|
)
|
|
228
232
|
if (!segmentationMatch) return null
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
return await this.userAssignmentDao.save({
|
|
235
|
+
userId,
|
|
236
|
+
experimentId: experiment.id,
|
|
237
|
+
bucketId: determineAssignment(experiment.sampling, experiment.buckets),
|
|
233
238
|
})
|
|
234
239
|
}
|
|
235
240
|
|
|
@@ -243,58 +248,9 @@ export class Abba {
|
|
|
243
248
|
private async getExistingUserAssignment(
|
|
244
249
|
experimentId: number,
|
|
245
250
|
userId: string,
|
|
246
|
-
): Promise<UserAssignment | null> {
|
|
247
|
-
|
|
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
|
-
}
|
|
297
|
-
|
|
298
|
-
return await Promise.all(savedBuckets)
|
|
251
|
+
): Promise<Saved<UserAssignment> | null> {
|
|
252
|
+
const assignments = await this.userAssignmentDao.getBy('userId', userId)
|
|
253
|
+
const assignment = assignments.find(assignment => assignment.experimentId === experimentId)
|
|
254
|
+
return assignment || null
|
|
299
255
|
}
|
|
300
256
|
}
|
|
@@ -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
|
@@ -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,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CommonDB } from '@naturalcycles/db-lib'
|
|
2
|
+
import { BaseDBEntity, Saved } from '@naturalcycles/js-lib'
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
4
|
+
export interface AbbaConfig {
|
|
5
|
+
db: CommonDB
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
8
|
+
export type BaseExperiment = BaseDBEntity<number> & {
|
|
9
|
+
name: string
|
|
10
|
+
status: number
|
|
11
|
+
sampling: number
|
|
12
|
+
description: string | null
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
export type
|
|
15
|
+
export type Experiment = BaseExperiment & {
|
|
16
|
+
rules: SegmentationRule[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ExperimentWithBuckets = Saved<Experiment> & {
|
|
20
|
+
buckets: Saved<Bucket>[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BucketInput {
|
|
24
|
+
experimentId: number
|
|
25
|
+
key: string
|
|
26
|
+
ratio: number
|
|
27
|
+
}
|
|
12
28
|
|
|
13
|
-
export type
|
|
29
|
+
export type Bucket = BaseDBEntity<number> & {
|
|
30
|
+
experimentId: number
|
|
31
|
+
key: string
|
|
32
|
+
ratio: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type UserAssignment = BaseDBEntity<number> & {
|
|
36
|
+
userId: string
|
|
37
|
+
experimentId: number
|
|
38
|
+
bucketId: number | null
|
|
39
|
+
}
|
|
14
40
|
|
|
15
41
|
export type SegmentationData = Record<string, string | boolean | number>
|
|
16
42
|
|
package/src/util.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Saved } from '@naturalcycles/js-lib'
|
|
1
2
|
import { satisfies } from 'semver'
|
|
2
|
-
import { Bucket } from './
|
|
3
|
-
import { BucketInput, SegmentationData, SegmentationRule } from '.'
|
|
3
|
+
import { Bucket, SegmentationData, SegmentationRule } from './types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate a random number between 0 and 100
|
|
@@ -18,7 +18,7 @@ export const rollDie = (): number => {
|
|
|
18
18
|
* @param buckets
|
|
19
19
|
* @returns
|
|
20
20
|
*/
|
|
21
|
-
export const determineAssignment = (sampling: number, buckets: Bucket[]): number | null => {
|
|
21
|
+
export const determineAssignment = (sampling: number, buckets: Saved<Bucket>[]): number | null => {
|
|
22
22
|
// Should this person be considered for the experiment?
|
|
23
23
|
if (rollDie() > sampling) {
|
|
24
24
|
return null
|
|
@@ -34,7 +34,7 @@ export const determineAssignment = (sampling: number, buckets: Bucket[]): number
|
|
|
34
34
|
* @param buckets
|
|
35
35
|
* @returns
|
|
36
36
|
*/
|
|
37
|
-
export const determineBucket = (buckets: Bucket[]): number => {
|
|
37
|
+
export const determineBucket = (buckets: Saved<Bucket>[]): number => {
|
|
38
38
|
const bucketRoll = rollDie()
|
|
39
39
|
let range: [number, number] | undefined
|
|
40
40
|
const bucket = buckets.find(b => {
|
|
@@ -62,7 +62,7 @@ export const determineBucket = (buckets: Bucket[]): number => {
|
|
|
62
62
|
* @param buckets
|
|
63
63
|
* @returns
|
|
64
64
|
*/
|
|
65
|
-
export const validateTotalBucketRatio = (buckets:
|
|
65
|
+
export const validateTotalBucketRatio = (buckets: Bucket[]): void => {
|
|
66
66
|
const bucketSum = buckets.reduce((sum, current) => sum + current.ratio, 0)
|
|
67
67
|
if (bucketSum !== 100) {
|
|
68
68
|
throw new Error('Total bucket ratio must be 100 before you can activate an experiment')
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, '__esModule', { value: true })
|
|
2
|
-
|
|
3
|
-
const { Decimal } = require('./runtime/index-browser')
|
|
4
|
-
|
|
5
|
-
const Prisma = {}
|
|
6
|
-
|
|
7
|
-
exports.Prisma = Prisma
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Prisma Client JS version: 3.13.0
|
|
11
|
-
* Query Engine version: efdf9b1183dddfd4258cd181a72125755215ab7b
|
|
12
|
-
*/
|
|
13
|
-
Prisma.prismaVersion = {
|
|
14
|
-
client: '3.13.0',
|
|
15
|
-
engine: 'efdf9b1183dddfd4258cd181a72125755215ab7b',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
Prisma.PrismaClientKnownRequestError = () => {
|
|
19
|
-
throw new Error(`PrismaClientKnownRequestError is unable to be run in the browser.
|
|
20
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
21
|
-
}
|
|
22
|
-
Prisma.PrismaClientUnknownRequestError = () => {
|
|
23
|
-
throw new Error(`PrismaClientUnknownRequestError is unable to be run in the browser.
|
|
24
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
25
|
-
}
|
|
26
|
-
Prisma.PrismaClientRustPanicError = () => {
|
|
27
|
-
throw new Error(`PrismaClientRustPanicError is unable to be run in the browser.
|
|
28
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
29
|
-
}
|
|
30
|
-
Prisma.PrismaClientInitializationError = () => {
|
|
31
|
-
throw new Error(`PrismaClientInitializationError is unable to be run in the browser.
|
|
32
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
33
|
-
}
|
|
34
|
-
Prisma.PrismaClientValidationError = () => {
|
|
35
|
-
throw new Error(`PrismaClientValidationError is unable to be run in the browser.
|
|
36
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
37
|
-
}
|
|
38
|
-
Prisma.Decimal = Decimal
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Re-export of sql-template-tag
|
|
42
|
-
*/
|
|
43
|
-
Prisma.sql = () => {
|
|
44
|
-
throw new Error(`sqltag is unable to be run in the browser.
|
|
45
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
46
|
-
}
|
|
47
|
-
Prisma.empty = () => {
|
|
48
|
-
throw new Error(`empty is unable to be run in the browser.
|
|
49
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
50
|
-
}
|
|
51
|
-
Prisma.join = () => {
|
|
52
|
-
throw new Error(`join is unable to be run in the browser.
|
|
53
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
54
|
-
}
|
|
55
|
-
Prisma.raw = () => {
|
|
56
|
-
throw new Error(`raw is unable to be run in the browser.
|
|
57
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`)
|
|
58
|
-
}
|
|
59
|
-
Prisma.validator = () => val => val
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Shorthand utilities for JSON filtering
|
|
63
|
-
*/
|
|
64
|
-
Prisma.DbNull = 'DbNull'
|
|
65
|
-
Prisma.JsonNull = 'JsonNull'
|
|
66
|
-
Prisma.AnyNull = 'AnyNull'
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Enums
|
|
70
|
-
*/
|
|
71
|
-
// Based on
|
|
72
|
-
// https://github.com/microsoft/TypeScript/issues/3192#issuecomment-261720275
|
|
73
|
-
function makeEnum(x) {
|
|
74
|
-
return x
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
exports.Prisma.BucketScalarFieldEnum = makeEnum({
|
|
78
|
-
id: 'id',
|
|
79
|
-
experimentId: 'experimentId',
|
|
80
|
-
key: 'key',
|
|
81
|
-
ratio: 'ratio',
|
|
82
|
-
createdAt: 'createdAt',
|
|
83
|
-
updatedAt: 'updatedAt',
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
exports.Prisma.ExperimentScalarFieldEnum = makeEnum({
|
|
87
|
-
id: 'id',
|
|
88
|
-
name: 'name',
|
|
89
|
-
status: 'status',
|
|
90
|
-
sampling: 'sampling',
|
|
91
|
-
createdAt: 'createdAt',
|
|
92
|
-
updatedAt: 'updatedAt',
|
|
93
|
-
description: 'description',
|
|
94
|
-
rules: 'rules',
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
exports.Prisma.UserAssignmentScalarFieldEnum = makeEnum({
|
|
98
|
-
id: 'id',
|
|
99
|
-
userId: 'userId',
|
|
100
|
-
experimentId: 'experimentId',
|
|
101
|
-
bucketId: 'bucketId',
|
|
102
|
-
createdAt: 'createdAt',
|
|
103
|
-
updatedAt: 'updatedAt',
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
exports.Prisma.SortOrder = makeEnum({
|
|
107
|
-
asc: 'asc',
|
|
108
|
-
desc: 'desc',
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
exports.Prisma.NullableJsonNullValueInput = makeEnum({
|
|
112
|
-
DbNull: 'DbNull',
|
|
113
|
-
JsonNull: 'JsonNull',
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
exports.Prisma.JsonNullValueFilter = makeEnum({
|
|
117
|
-
DbNull: 'DbNull',
|
|
118
|
-
JsonNull: 'JsonNull',
|
|
119
|
-
AnyNull: 'AnyNull',
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
exports.Prisma.ModelName = makeEnum({
|
|
123
|
-
Bucket: 'Bucket',
|
|
124
|
-
Experiment: 'Experiment',
|
|
125
|
-
UserAssignment: 'UserAssignment',
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Create the Client
|
|
130
|
-
*/
|
|
131
|
-
class PrismaClient {
|
|
132
|
-
constructor() {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`PrismaClient is unable to be run in the browser.
|
|
135
|
-
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues`,
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
exports.PrismaClient = PrismaClient
|
|
140
|
-
|
|
141
|
-
Object.assign(exports, Prisma)
|