@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.
- package/dist/abba.d.ts +45 -72
- package/dist/abba.js +119 -165
- 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 -21
- package/dist/util.js +0 -16
- package/package.json +9 -9
- package/readme.md +14 -15
- package/src/abba.ts +160 -191
- 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 +41 -7
- package/src/util.ts +5 -21
- 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,300 +1,269 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
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
|
-
*
|
|
35
|
+
* Cold method, not cached.
|
|
33
36
|
*/
|
|
34
|
-
async getAllExperiments(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
const updatedBuckets = await this.saveBuckets(buckets)
|
|
116
|
+
await this.experimentDao.save(experiment)
|
|
93
117
|
|
|
94
118
|
return {
|
|
95
|
-
...
|
|
96
|
-
buckets:
|
|
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.
|
|
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
|
|
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
|
-
|
|
153
|
+
const experiment = await this.experimentDao.requireById(experimentId)
|
|
154
|
+
if (experiment.status !== AssignmentStatus.Active) return null
|
|
134
155
|
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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.
|
|
187
|
+
): Promise<Saved<UserAssignment>[]> {
|
|
188
|
+
const experiments = await this.getActiveExperiments() // cached
|
|
163
189
|
const existingAssignments = await this.getAllExistingUserAssignments(userId)
|
|
164
190
|
|
|
165
|
-
const
|
|
166
|
-
const generatedAssignments:
|
|
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
|
-
|
|
172
|
-
continue
|
|
197
|
+
allAssignments.push(existing)
|
|
173
198
|
} else {
|
|
174
|
-
|
|
199
|
+
const assignment = this.generateUserAssignmentData(experiment, userId, segmentationData)
|
|
200
|
+
if (assignment) {
|
|
201
|
+
generatedAssignments.push(assignment)
|
|
202
|
+
}
|
|
175
203
|
}
|
|
176
204
|
}
|
|
177
205
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return [...
|
|
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.
|
|
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.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
239
|
+
private generateUserAssignmentData(
|
|
220
240
|
experiment: ExperimentWithBuckets,
|
|
221
241
|
userId: string,
|
|
222
242
|
segmentationData: SegmentationData,
|
|
223
|
-
):
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
@@ -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 {
|
|
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
|
|
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
|
|
8
|
-
|
|
23
|
+
export type Experiment = BaseExperiment & {
|
|
24
|
+
rules: SegmentationRule[]
|
|
9
25
|
}
|
|
10
26
|
|
|
11
|
-
export type
|
|
27
|
+
export type ExperimentWithBuckets = Saved<Experiment> & {
|
|
28
|
+
buckets: Saved<Bucket>[]
|
|
29
|
+
}
|
|
12
30
|
|
|
13
|
-
export
|
|
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
|
|