@prosopo/database 3.0.8 → 3.0.10

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.
@@ -1,1073 +1,1278 @@
1
1
  import { isHex } from "@polkadot/util/is";
2
2
  import { ProsopoDBError } from "@prosopo/common";
3
- import { ApiParams, CaptchaStates, CaptchaStatus, DatasetWithIdsAndTreeSchema, StoredStatusNames, } from "@prosopo/types";
4
- import { CaptchaRecordSchema, ClientRecordSchema, DatasetRecordSchema, DetectorRecordSchema, FrictionlessTokenRecordSchema, PendingRecordSchema, PoWCaptchaRecordSchema, ScheduledTaskRecordSchema, ScheduledTaskSchema, SessionRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserCommitmentSchema, UserSolutionRecordSchema, } from "@prosopo/types-database";
5
- import { createRedisAccessRulesIndex, createRedisAccessRulesStorage, } from "@prosopo/user-access-policy";
3
+ import { DatasetWithIdsAndTreeSchema, StoredStatusNames, CaptchaStatus, ApiParams, CaptchaStates } from "@prosopo/types";
4
+ import { CaptchaRecordSchema, PoWCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema, PendingRecordSchema, ScheduledTaskRecordSchema, ClientRecordSchema, FrictionlessTokenRecordSchema, SessionRecordSchema, DetectorRecordSchema, UserCommitmentSchema, ScheduledTaskSchema } from "@prosopo/types-database";
5
+ import { createRedisAccessRulesIndex, createRedisAccessRulesStorage } from "@prosopo/user-access-policy";
6
6
  import { createClient } from "redis";
7
7
  import { MongoDatabase } from "../base/mongo.js";
8
- var TableNames;
9
- (function (TableNames) {
10
- TableNames["captcha"] = "captcha";
11
- TableNames["dataset"] = "dataset";
12
- TableNames["solution"] = "solution";
13
- TableNames["commitment"] = "commitment";
14
- TableNames["usersolution"] = "usersolution";
15
- TableNames["pending"] = "pending";
16
- TableNames["scheduler"] = "scheduler";
17
- TableNames["powcaptcha"] = "powcaptcha";
18
- TableNames["client"] = "client";
19
- TableNames["frictionlessToken"] = "frictionlessToken";
20
- TableNames["session"] = "session";
21
- TableNames["detector"] = "detector";
22
- })(TableNames || (TableNames = {}));
8
+ var TableNames = /* @__PURE__ */ ((TableNames2) => {
9
+ TableNames2["captcha"] = "captcha";
10
+ TableNames2["dataset"] = "dataset";
11
+ TableNames2["solution"] = "solution";
12
+ TableNames2["commitment"] = "commitment";
13
+ TableNames2["usersolution"] = "usersolution";
14
+ TableNames2["pending"] = "pending";
15
+ TableNames2["scheduler"] = "scheduler";
16
+ TableNames2["powcaptcha"] = "powcaptcha";
17
+ TableNames2["client"] = "client";
18
+ TableNames2["frictionlessToken"] = "frictionlessToken";
19
+ TableNames2["session"] = "session";
20
+ TableNames2["detector"] = "detector";
21
+ return TableNames2;
22
+ })(TableNames || {});
23
23
  const PROVIDER_TABLES = [
24
- {
25
- collectionName: TableNames.captcha,
26
- modelName: "Captcha",
27
- schema: CaptchaRecordSchema,
28
- },
29
- {
30
- collectionName: TableNames.powcaptcha,
31
- modelName: "PowCaptcha",
32
- schema: PoWCaptchaRecordSchema,
33
- },
34
- {
35
- collectionName: TableNames.dataset,
36
- modelName: "Dataset",
37
- schema: DatasetRecordSchema,
38
- },
39
- {
40
- collectionName: TableNames.solution,
41
- modelName: "Solution",
42
- schema: SolutionRecordSchema,
43
- },
44
- {
45
- collectionName: TableNames.commitment,
46
- modelName: "UserCommitment",
47
- schema: UserCommitmentRecordSchema,
48
- },
49
- {
50
- collectionName: TableNames.usersolution,
51
- modelName: "UserSolution",
52
- schema: UserSolutionRecordSchema,
53
- },
54
- {
55
- collectionName: TableNames.pending,
56
- modelName: "Pending",
57
- schema: PendingRecordSchema,
58
- },
59
- {
60
- collectionName: TableNames.scheduler,
61
- modelName: "Scheduler",
62
- schema: ScheduledTaskRecordSchema,
63
- },
64
- {
65
- collectionName: TableNames.client,
66
- modelName: "Client",
67
- schema: ClientRecordSchema,
68
- },
69
- {
70
- collectionName: TableNames.frictionlessToken,
71
- modelName: "FrictionlessToken",
72
- schema: FrictionlessTokenRecordSchema,
73
- },
74
- {
75
- collectionName: TableNames.session,
76
- modelName: "Session",
77
- schema: SessionRecordSchema,
78
- },
79
- {
80
- collectionName: TableNames.detector,
81
- modelName: "Detector",
82
- schema: DetectorRecordSchema,
83
- },
24
+ {
25
+ collectionName: "captcha",
26
+ modelName: "Captcha",
27
+ schema: CaptchaRecordSchema
28
+ },
29
+ {
30
+ collectionName: "powcaptcha",
31
+ modelName: "PowCaptcha",
32
+ schema: PoWCaptchaRecordSchema
33
+ },
34
+ {
35
+ collectionName: "dataset",
36
+ modelName: "Dataset",
37
+ schema: DatasetRecordSchema
38
+ },
39
+ {
40
+ collectionName: "solution",
41
+ modelName: "Solution",
42
+ schema: SolutionRecordSchema
43
+ },
44
+ {
45
+ collectionName: "commitment",
46
+ modelName: "UserCommitment",
47
+ schema: UserCommitmentRecordSchema
48
+ },
49
+ {
50
+ collectionName: "usersolution",
51
+ modelName: "UserSolution",
52
+ schema: UserSolutionRecordSchema
53
+ },
54
+ {
55
+ collectionName: "pending",
56
+ modelName: "Pending",
57
+ schema: PendingRecordSchema
58
+ },
59
+ {
60
+ collectionName: "scheduler",
61
+ modelName: "Scheduler",
62
+ schema: ScheduledTaskRecordSchema
63
+ },
64
+ {
65
+ collectionName: "client",
66
+ modelName: "Client",
67
+ schema: ClientRecordSchema
68
+ },
69
+ {
70
+ collectionName: "frictionlessToken",
71
+ modelName: "FrictionlessToken",
72
+ schema: FrictionlessTokenRecordSchema
73
+ },
74
+ {
75
+ collectionName: "session",
76
+ modelName: "Session",
77
+ schema: SessionRecordSchema
78
+ },
79
+ {
80
+ collectionName: "detector",
81
+ modelName: "Detector",
82
+ schema: DetectorRecordSchema
83
+ }
84
84
  ];
85
- export class ProviderDatabase extends MongoDatabase {
86
- constructor(options) {
87
- super(options.mongo.url, options.mongo.dbname, options.mongo.authSource, options.logger);
88
- this.options = options;
89
- this.tables = {};
90
- this.tables = {};
91
- this.userAccessRulesStorage = null;
85
+ class ProviderDatabase extends MongoDatabase {
86
+ constructor(options) {
87
+ super(
88
+ options.mongo.url,
89
+ options.mongo.dbname,
90
+ options.mongo.authSource,
91
+ options.logger
92
+ );
93
+ this.options = options;
94
+ this.tables = {};
95
+ this.tables = {};
96
+ this.userAccessRulesStorage = null;
97
+ }
98
+ async connect() {
99
+ await super.connect();
100
+ this.loadTables();
101
+ await this.setupRedis();
102
+ }
103
+ async setupRedis() {
104
+ const redisClient = await this.createRedisClient();
105
+ await createRedisAccessRulesIndex(redisClient);
106
+ this.userAccessRulesStorage = createRedisAccessRulesStorage(
107
+ redisClient,
108
+ this.logger
109
+ );
110
+ }
111
+ async createRedisClient() {
112
+ return await createClient({
113
+ url: this.options.redis?.url,
114
+ password: this.options.redis?.password
115
+ }).on("error", (error) => {
116
+ this.logger.error(() => ({
117
+ err: error,
118
+ msg: "Redis client error"
119
+ }));
120
+ }).connect();
121
+ }
122
+ loadTables() {
123
+ const tables = {};
124
+ PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => {
125
+ if (this.connection) {
126
+ tables[collectionName] = this.connection.model(modelName, schema);
127
+ }
128
+ });
129
+ this.tables = tables;
130
+ }
131
+ getTables() {
132
+ if (!this.tables) {
133
+ throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
134
+ context: { failedFuncName: this.getTables.name },
135
+ logger: this.logger
136
+ });
92
137
  }
93
- async connect() {
94
- await super.connect();
95
- this.loadTables();
96
- await this.setupRedis();
138
+ return this.tables;
139
+ }
140
+ getUserAccessRulesStorage() {
141
+ if (null === this.userAccessRulesStorage) {
142
+ throw new ProsopoDBError("DATABASE.USER_ACCESS_RULES_STORAGE_UNDEFINED");
97
143
  }
98
- async setupRedis() {
99
- const redisClient = await this.createRedisClient();
100
- await createRedisAccessRulesIndex(redisClient);
101
- this.userAccessRulesStorage = createRedisAccessRulesStorage(redisClient, this.logger);
102
- }
103
- async createRedisClient() {
104
- return (await createClient({
105
- url: this.options.redis?.url,
106
- password: this.options.redis?.password,
107
- })
108
- .on("error", (error) => {
109
- this.logger.error(() => ({
110
- err: error,
111
- msg: "Redis client error",
112
- }));
144
+ return this.userAccessRulesStorage;
145
+ }
146
+ /**
147
+ * @description Load a dataset to the database
148
+ * @param {Dataset} dataset
149
+ */
150
+ async storeDataset(dataset) {
151
+ try {
152
+ this.logger.debug(() => ({
153
+ data: { datasetId: dataset.datasetId },
154
+ msg: "Storing dataset in database"
155
+ }));
156
+ const parsedDataset = DatasetWithIdsAndTreeSchema.parse(dataset);
157
+ const datasetDoc = {
158
+ datasetId: parsedDataset.datasetId,
159
+ datasetContentId: parsedDataset.datasetContentId,
160
+ format: parsedDataset.format,
161
+ contentTree: parsedDataset.contentTree,
162
+ solutionTree: parsedDataset.solutionTree
163
+ };
164
+ const filter = {
165
+ datasetId: parsedDataset.datasetId
166
+ };
167
+ await this.tables.dataset?.updateOne(
168
+ filter,
169
+ { $set: datasetDoc },
170
+ { upsert: true }
171
+ );
172
+ const captchaDocs = parsedDataset.captchas.map(
173
+ ({ solution, ...captcha }, index) => ({
174
+ ...captcha,
175
+ datasetId: parsedDataset.datasetId,
176
+ datasetContentId: parsedDataset.datasetContentId,
177
+ index,
178
+ solved: !!solution?.length
113
179
  })
114
- .connect());
115
- }
116
- loadTables() {
117
- const tables = {};
118
- PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => {
119
- if (this.connection) {
120
- tables[collectionName] = this.connection.model(modelName, schema);
121
- }
122
- });
123
- this.tables = tables;
124
- }
125
- getTables() {
126
- if (!this.tables) {
127
- throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
128
- context: { failedFuncName: this.getTables.name },
129
- logger: this.logger,
130
- });
131
- }
132
- return this.tables;
133
- }
134
- getUserAccessRulesStorage() {
135
- if (null === this.userAccessRulesStorage) {
136
- throw new ProsopoDBError("DATABASE.USER_ACCESS_RULES_STORAGE_UNDEFINED");
137
- }
138
- return this.userAccessRulesStorage;
139
- }
140
- async storeDataset(dataset) {
141
- try {
142
- this.logger.debug(() => ({
143
- data: { datasetId: dataset.datasetId },
144
- msg: "Storing dataset in database",
145
- }));
146
- const parsedDataset = DatasetWithIdsAndTreeSchema.parse(dataset);
147
- const datasetDoc = {
148
- datasetId: parsedDataset.datasetId,
149
- datasetContentId: parsedDataset.datasetContentId,
150
- format: parsedDataset.format,
151
- contentTree: parsedDataset.contentTree,
152
- solutionTree: parsedDataset.solutionTree,
153
- };
154
- const filter = {
155
- datasetId: parsedDataset.datasetId,
156
- };
157
- await this.tables.dataset?.updateOne(filter, { $set: datasetDoc }, { upsert: true });
158
- const captchaDocs = parsedDataset.captchas.map(({ solution, ...captcha }, index) => ({
159
- ...captcha,
160
- datasetId: parsedDataset.datasetId,
161
- datasetContentId: parsedDataset.datasetContentId,
162
- index,
163
- solved: !!solution?.length,
164
- }));
165
- this.logger.debug(() => ({
166
- msg: "Inserting captcha records",
167
- }));
168
- if (captchaDocs.length) {
169
- await this.tables?.captcha.bulkWrite(captchaDocs.map((captchaDoc) => ({
170
- updateOne: {
171
- filter: { captchaId: captchaDoc.captchaId },
172
- update: { $set: captchaDoc },
173
- upsert: true,
174
- },
175
- })));
180
+ );
181
+ this.logger.debug(() => ({
182
+ msg: "Inserting captcha records"
183
+ }));
184
+ if (captchaDocs.length) {
185
+ await this.tables?.captcha.bulkWrite(
186
+ captchaDocs.map((captchaDoc) => ({
187
+ updateOne: {
188
+ filter: { captchaId: captchaDoc.captchaId },
189
+ update: { $set: captchaDoc },
190
+ upsert: true
176
191
  }
177
- const captchaSolutionDocs = parsedDataset.captchas
178
- .filter(({ solution }) => solution?.length)
179
- .map((captcha) => ({
180
- captchaId: captcha.captchaId,
181
- captchaContentId: captcha.captchaContentId,
182
- solution: captcha.solution,
183
- salt: captcha.salt,
184
- datasetId: parsedDataset.datasetId,
185
- datasetContentId: parsedDataset.datasetContentId,
186
- }));
187
- this.logger.debug(() => ({
188
- msg: "Inserting solution records",
189
- }));
190
- if (captchaSolutionDocs.length) {
191
- await this.tables?.solution.bulkWrite(captchaSolutionDocs.map((captchaSolutionDoc) => ({
192
- updateOne: {
193
- filter: { captchaId: captchaSolutionDoc.captchaId },
194
- update: { $set: captchaSolutionDoc },
195
- upsert: true,
196
- },
197
- })));
192
+ }))
193
+ );
194
+ }
195
+ const captchaSolutionDocs = parsedDataset.captchas.filter(({ solution }) => solution?.length).map((captcha) => ({
196
+ captchaId: captcha.captchaId,
197
+ captchaContentId: captcha.captchaContentId,
198
+ solution: captcha.solution,
199
+ salt: captcha.salt,
200
+ datasetId: parsedDataset.datasetId,
201
+ datasetContentId: parsedDataset.datasetContentId
202
+ }));
203
+ this.logger.debug(() => ({
204
+ msg: "Inserting solution records"
205
+ }));
206
+ if (captchaSolutionDocs.length) {
207
+ await this.tables?.solution.bulkWrite(
208
+ captchaSolutionDocs.map((captchaSolutionDoc) => ({
209
+ updateOne: {
210
+ filter: { captchaId: captchaSolutionDoc.captchaId },
211
+ update: { $set: captchaSolutionDoc },
212
+ upsert: true
198
213
  }
199
- this.logger.debug(() => ({
200
- msg: "Dataset stored in database",
201
- }));
202
- }
203
- catch (err) {
204
- throw new ProsopoDBError("DATABASE.DATASET_LOAD_FAILED", {
205
- context: { failedFuncName: this.storeDataset.name, error: err },
206
- logger: this.logger,
207
- });
208
- }
209
- }
210
- async getSolutions(datasetId) {
211
- const filter = { datasetId };
212
- const docs = await this.tables?.solution
213
- .find(filter)
214
- .lean();
215
- return docs ? docs : [];
216
- }
217
- async getSolutionByCaptchaId(captchaId) {
218
- const filter = { captchaId };
219
- const doc = await this.tables?.solution
220
- .findOne(filter)
221
- .lean();
222
- return doc || null;
214
+ }))
215
+ );
216
+ }
217
+ this.logger.debug(() => ({
218
+ msg: "Dataset stored in database"
219
+ }));
220
+ } catch (err) {
221
+ throw new ProsopoDBError("DATABASE.DATASET_LOAD_FAILED", {
222
+ context: { failedFuncName: this.storeDataset.name, error: err },
223
+ logger: this.logger
224
+ });
223
225
  }
224
- async getDataset(datasetId) {
225
- const filter = { datasetId };
226
- const datasetDoc = await this.tables?.dataset.findOne(filter).lean();
227
- if (datasetDoc) {
228
- const { datasetContentId, format, contentTree, solutionTree } = datasetDoc;
229
- const captchas = (await this.tables?.captcha.find(filter).lean()) || [];
230
- const solutions = (await this.tables?.solution.find(filter).lean()) ||
231
- [];
232
- const solutionsKeyed = {};
233
- for (const solution of solutions) {
234
- solutionsKeyed[solution.captchaId] = solution;
235
- }
236
- return {
237
- datasetId,
238
- datasetContentId,
239
- format,
240
- contentTree: contentTree || [],
241
- solutionTree: solutionTree || [],
242
- captchas: captchas.map((captchaDoc) => {
243
- const { captchaId, captchaContentId, items, target, salt, solved } = captchaDoc;
244
- const solution = solutionsKeyed[captchaId];
245
- return {
246
- captchaId,
247
- captchaContentId,
248
- solved: !!solved,
249
- salt,
250
- items,
251
- target,
252
- solution: solved && solution ? solution.solution : [],
253
- };
254
- }),
255
- };
256
- }
257
- throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
258
- context: { failedFuncName: this.getDataset.name, datasetId },
259
- });
260
- }
261
- async getRandomCaptcha(solved, datasetId, size) {
262
- if (!isHex(datasetId)) {
263
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
264
- context: { failedFuncName: this.getRandomCaptcha.name, datasetId },
265
- });
266
- }
267
- const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
268
- const filter = { datasetId, solved };
269
- const cursor = this.tables?.captcha.aggregate([
270
- { $match: filter },
271
- { $sample: { size: sampleSize } },
272
- {
273
- $project: {
274
- datasetId: 1,
275
- datasetContentId: 1,
276
- captchaId: 1,
277
- captchaContentId: 1,
278
- items: 1,
279
- target: 1,
280
- },
281
- },
282
- ]);
283
- const docs = await cursor;
284
- if (docs?.length) {
285
- return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
286
- }
287
- throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
288
- context: {
289
- failedFuncName: this.getRandomCaptcha.name,
290
- solved,
291
- datasetId,
292
- size,
293
- },
294
- });
226
+ }
227
+ /** @description Get solutions for a dataset
228
+ * @param {string} datasetId
229
+ */
230
+ async getSolutions(datasetId) {
231
+ const filter = { datasetId };
232
+ const docs = await this.tables?.solution.find(filter).lean();
233
+ return docs ? docs : [];
234
+ }
235
+ /** @description Get a solution by captcha id
236
+ * @param {string} captchaId
237
+ */
238
+ async getSolutionByCaptchaId(captchaId) {
239
+ const filter = { captchaId };
240
+ const doc = await this.tables?.solution.findOne(filter).lean();
241
+ return doc || null;
242
+ }
243
+ /** @description Get a dataset from the database
244
+ * @param {string} datasetId
245
+ */
246
+ async getDataset(datasetId) {
247
+ const filter = { datasetId };
248
+ const datasetDoc = await this.tables?.dataset.findOne(filter).lean();
249
+ if (datasetDoc) {
250
+ const { datasetContentId, format, contentTree, solutionTree } = datasetDoc;
251
+ const captchas = await this.tables?.captcha.find(filter).lean() || [];
252
+ const solutions = await this.tables?.solution.find(filter).lean() || [];
253
+ const solutionsKeyed = {};
254
+ for (const solution of solutions) {
255
+ solutionsKeyed[solution.captchaId] = solution;
256
+ }
257
+ return {
258
+ datasetId,
259
+ datasetContentId,
260
+ format,
261
+ contentTree: contentTree || [],
262
+ solutionTree: solutionTree || [],
263
+ captchas: captchas.map((captchaDoc) => {
264
+ const { captchaId, captchaContentId, items, target, salt, solved } = captchaDoc;
265
+ const solution = solutionsKeyed[captchaId];
266
+ return {
267
+ captchaId,
268
+ captchaContentId,
269
+ solved: !!solved,
270
+ salt,
271
+ items,
272
+ target,
273
+ solution: solved && solution ? solution.solution : []
274
+ };
275
+ })
276
+ };
295
277
  }
296
- async getCaptchaById(captchaId) {
297
- const filter = { captchaId: { $in: captchaId } };
298
- const cursor = this.tables?.captcha
299
- .find(filter)
300
- .lean();
301
- const docs = await cursor;
302
- if (docs?.length) {
303
- return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
304
- }
305
- throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
306
- context: { failedFuncName: this.getCaptchaById.name, captchaId },
307
- });
278
+ throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
279
+ context: { failedFuncName: this.getDataset.name, datasetId }
280
+ });
281
+ }
282
+ /**
283
+ * @description Get random captchas that are solved or not solved
284
+ * @param {boolean} solved `true` when captcha is solved
285
+ * @param {string} datasetId the id of the data set
286
+ * @param {number} size the number of records to be returned
287
+ */
288
+ async getRandomCaptcha(solved, datasetId, size) {
289
+ if (!isHex(datasetId)) {
290
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
291
+ context: { failedFuncName: this.getRandomCaptcha.name, datasetId }
292
+ });
308
293
  }
309
- async updateCaptcha(captcha, datasetId) {
310
- if (!isHex(datasetId)) {
311
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
312
- context: { failedFuncName: this.updateCaptcha.name, datasetId },
313
- });
314
- }
315
- try {
316
- const filter = { datasetId };
317
- await this.tables?.captcha.updateOne(filter, { $set: captcha }, { upsert: false });
318
- }
319
- catch (err) {
320
- throw new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
321
- context: { failedFuncName: this.getDatasetDetails.name, error: err },
322
- });
294
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
295
+ const filter = { datasetId, solved };
296
+ const cursor = this.tables?.captcha.aggregate([
297
+ { $match: filter },
298
+ { $sample: { size: sampleSize } },
299
+ {
300
+ $project: {
301
+ datasetId: 1,
302
+ datasetContentId: 1,
303
+ captchaId: 1,
304
+ captchaContentId: 1,
305
+ items: 1,
306
+ target: 1
323
307
  }
308
+ }
309
+ ]);
310
+ const docs = await cursor;
311
+ if (docs?.length) {
312
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
324
313
  }
325
- async removeCaptchas(captchaIds) {
326
- const filter = { captchaId: { $in: captchaIds } };
327
- await this.tables?.captcha.deleteMany(filter);
314
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
315
+ context: {
316
+ failedFuncName: this.getRandomCaptcha.name,
317
+ solved,
318
+ datasetId,
319
+ size
320
+ }
321
+ });
322
+ }
323
+ /**
324
+ * @description Get captchas by id
325
+ * @param {string[]} captchaId
326
+ */
327
+ async getCaptchaById(captchaId) {
328
+ const filter = { captchaId: { $in: captchaId } };
329
+ const cursor = this.tables?.captcha.find(filter).lean();
330
+ const docs = await cursor;
331
+ if (docs?.length) {
332
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
328
333
  }
329
- async getDatasetDetails(datasetId) {
330
- if (!isHex(datasetId)) {
331
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
332
- context: { failedFuncName: this.getDatasetDetails.name, datasetId },
333
- });
334
- }
335
- const filter = { datasetId };
336
- const doc = await this.tables?.dataset
337
- .findOne(filter)
338
- .lean();
339
- if (doc) {
340
- return doc;
341
- }
342
- throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
343
- context: {
344
- failedFuncName: this.getDatasetDetails.name,
345
- datasetId,
346
- },
347
- });
334
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
335
+ context: { failedFuncName: this.getCaptchaById.name, captchaId }
336
+ });
337
+ }
338
+ /**
339
+ * @description Update a captcha
340
+ * @param {Captcha} captcha
341
+ * @param {string} datasetId the id of the data set
342
+ */
343
+ async updateCaptcha(captcha, datasetId) {
344
+ if (!isHex(datasetId)) {
345
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
346
+ context: { failedFuncName: this.updateCaptcha.name, datasetId }
347
+ });
348
348
  }
349
- async storeUserImageCaptchaSolution(captchas, commit) {
350
- const commitmentRecord = UserCommitmentSchema.parse({
351
- ...commit,
352
- lastUpdatedTimestamp: Date.now(),
353
- });
354
- if (captchas.length) {
355
- const filter = {
356
- id: commit.id,
357
- };
358
- await this.tables?.commitment.updateOne(filter, commitmentRecord, {
359
- upsert: true,
360
- });
361
- const ops = captchas.map((captcha) => ({
362
- updateOne: {
363
- filter: {
364
- commitmentId: commit.id,
365
- captchaId: captcha.captchaId,
366
- },
367
- update: {
368
- $set: {
369
- captchaId: captcha.captchaId,
370
- captchaContentId: captcha.captchaContentId,
371
- salt: captcha.salt,
372
- solution: captcha.solution,
373
- commitmentId: commit.id,
374
- processed: false,
375
- },
376
- },
377
- upsert: true,
378
- },
379
- }));
380
- await this.tables?.usersolution.bulkWrite(ops);
381
- }
349
+ try {
350
+ const filter = { datasetId };
351
+ await this.tables?.captcha.updateOne(
352
+ filter,
353
+ { $set: captcha },
354
+ { upsert: false }
355
+ );
356
+ } catch (err) {
357
+ throw new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
358
+ context: { failedFuncName: this.getDatasetDetails.name, error: err }
359
+ });
382
360
  }
383
- async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, frictionlessTokenId, serverChecked = false, userSubmitted = false, storedStatus = StoredStatusNames.notStored, userSignature) {
384
- const tables = this.getTables();
385
- const powCaptchaRecord = {
386
- challenge,
387
- ...components,
388
- ipAddress,
389
- headers,
390
- ja4,
391
- result: { status: CaptchaStatus.pending },
392
- userSubmitted,
393
- serverChecked,
394
- difficulty,
395
- providerSignature,
396
- userSignature,
397
- lastUpdatedTimestamp: Date.now(),
398
- frictionlessTokenId,
399
- };
400
- try {
401
- await tables.powcaptcha.create(powCaptchaRecord);
402
- this.logger.info(() => ({
403
- data: {
404
- challenge,
405
- userSubmitted,
406
- serverChecked,
407
- storedStatus,
408
- },
409
- msg: "PowCaptcha record added successfully",
410
- }));
411
- }
412
- catch (error) {
413
- const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
414
- context: {
415
- error,
416
- challenge,
417
- userSubmitted,
418
- serverChecked,
419
- storedStatus,
420
- },
421
- logger: this.logger,
422
- });
423
- this.logger.error(() => ({
424
- err: error,
425
- msg: "Failed to add PowCaptcha record",
426
- }));
427
- throw err;
428
- }
361
+ }
362
+ /**
363
+ * @description Remove captchas
364
+ */
365
+ async removeCaptchas(captchaIds) {
366
+ const filter = { captchaId: { $in: captchaIds } };
367
+ await this.tables?.captcha.deleteMany(filter);
368
+ }
369
+ /**
370
+ * @description Get a dataset by Id
371
+ */
372
+ async getDatasetDetails(datasetId) {
373
+ if (!isHex(datasetId)) {
374
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
375
+ context: { failedFuncName: this.getDatasetDetails.name, datasetId }
376
+ });
429
377
  }
430
- async getPowCaptchaRecordByChallenge(challenge) {
431
- if (!this.tables) {
432
- throw new ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
433
- context: { failedFuncName: this.getPowCaptchaRecordByChallenge.name },
434
- logger: this.logger,
435
- });
436
- }
437
- try {
438
- const filter = { challenge };
439
- const record = await this.tables.powcaptcha.findOne(filter).lean();
440
- if (record) {
441
- this.logger.info(() => ({
442
- data: { challenge },
443
- msg: "PowCaptcha record retrieved successfully",
444
- }));
445
- return record;
446
- }
447
- this.logger.info(() => ({
448
- data: { challenge },
449
- msg: "No PowCaptcha record found",
450
- }));
451
- return null;
452
- }
453
- catch (error) {
454
- const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
455
- context: { error, challenge },
456
- logger: this.logger,
457
- });
458
- this.logger.error(() => ({
459
- err: err,
460
- msg: "Failed to retrieve PowCaptcha record",
461
- }));
462
- throw err;
463
- }
378
+ const filter = { datasetId };
379
+ const doc = await this.tables?.dataset.findOne(filter).lean();
380
+ if (doc) {
381
+ return doc;
464
382
  }
465
- async updatePowCaptchaRecord(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
466
- const tables = this.getTables();
467
- const timestamp = Date.now();
468
- const update = {
469
- result,
470
- serverChecked,
471
- userSubmitted,
472
- userSignature,
473
- lastUpdatedTimestamp: timestamp,
474
- };
475
- try {
476
- const updateResult = await tables.powcaptcha.updateOne({ challenge }, {
477
- $set: update,
478
- });
479
- if (updateResult.matchedCount === 0) {
480
- const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
481
- context: {
482
- challenge,
483
- ...update,
484
- },
485
- logger: this.logger,
486
- });
487
- this.logger.info(() => ({
488
- err: err,
489
- msg: "No PowCaptcha record found to update",
490
- }));
491
- throw err;
383
+ throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
384
+ context: {
385
+ failedFuncName: this.getDatasetDetails.name,
386
+ datasetId
387
+ }
388
+ });
389
+ }
390
+ /**
391
+ * @description Store a Dapp User's captcha solution commitment
392
+ */
393
+ async storeUserImageCaptchaSolution(captchas, commit) {
394
+ const commitmentRecord = UserCommitmentSchema.parse({
395
+ ...commit,
396
+ lastUpdatedTimestamp: Date.now()
397
+ });
398
+ if (captchas.length) {
399
+ const filter = {
400
+ id: commit.id
401
+ };
402
+ await this.tables?.commitment.updateOne(filter, commitmentRecord, {
403
+ upsert: true
404
+ });
405
+ const ops = captchas.map((captcha) => ({
406
+ updateOne: {
407
+ filter: {
408
+ commitmentId: commit.id,
409
+ captchaId: captcha.captchaId
410
+ },
411
+ update: {
412
+ $set: {
413
+ captchaId: captcha.captchaId,
414
+ captchaContentId: captcha.captchaContentId,
415
+ salt: captcha.salt,
416
+ solution: captcha.solution,
417
+ commitmentId: commit.id,
418
+ processed: false
492
419
  }
493
- this.logger.info(() => ({
494
- data: {
495
- challenge,
496
- ...update,
497
- },
498
- msg: "PowCaptcha record updated successfully",
499
- }));
420
+ },
421
+ upsert: true
500
422
  }
501
- catch (error) {
502
- const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
503
- context: {
504
- error,
505
- challenge,
506
- ...update,
507
- },
508
- logger: this.logger,
509
- });
510
- this.logger.error(() => ({
511
- err: err,
512
- msg: "Failed to update PowCaptcha record",
513
- }));
514
- throw err;
515
- }
516
- }
517
- async getCheckedDappUserCommitments() {
518
- const filter = { [StoredStatusNames.serverChecked]: true };
519
- const docs = await this.tables?.commitment
520
- .find(filter)
521
- .lean();
522
- return docs || [];
523
- }
524
- async getUnstoredDappUserCommitments(limit = 1000, skip = 0) {
525
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
526
- const docs = await this.tables?.commitment.aggregate([
527
- {
528
- $match: {
529
- $or: [
530
- filterNoStoredTimestamp,
531
- {
532
- $expr: {
533
- $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"],
534
- },
535
- },
536
- ],
537
- },
538
- },
539
- {
540
- $sort: { _id: 1 },
541
- },
542
- {
543
- $skip: skip,
544
- },
545
- {
546
- $limit: limit,
547
- },
548
- ]);
549
- return docs || [];
423
+ }));
424
+ await this.tables?.usersolution.bulkWrite(ops);
550
425
  }
551
- async markDappUserCommitmentsStored(commitmentIds) {
552
- const updateDoc = {
553
- storedAtTimestamp: Date.now(),
554
- };
555
- await this.tables?.commitment.updateMany({ id: { $in: commitmentIds } }, { $set: updateDoc }, { upsert: false });
556
- }
557
- async markDappUserCommitmentsChecked(commitmentIds) {
558
- const updateDoc = {
559
- [StoredStatusNames.serverChecked]: true,
560
- lastUpdatedTimestamp: Date.now(),
561
- };
562
- await this.tables?.commitment.updateMany({ id: { $in: commitmentIds } }, { $set: updateDoc }, { upsert: false });
563
- }
564
- async getUnstoredDappUserPoWCommitments(limit = 1000, skip = 0) {
565
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
566
- const docs = await this.tables?.powcaptcha.aggregate([
567
- {
568
- $match: {
569
- $or: [
570
- filterNoStoredTimestamp,
571
- {
572
- $expr: {
573
- $lt: [
574
- {
575
- $convert: {
576
- input: "$storedAtTimestamp",
577
- to: "date",
578
- },
579
- },
580
- {
581
- $convert: {
582
- input: "$lastUpdatedTimestamp",
583
- to: "date",
584
- },
585
- },
586
- ],
587
- },
588
- },
589
- ],
590
- },
591
- },
592
- {
593
- $sort: { _id: 1 },
594
- },
595
- {
596
- $skip: skip,
597
- },
598
- {
599
- $limit: limit,
600
- },
601
- ]);
602
- return docs || [];
426
+ }
427
+ /**
428
+ * @description Adds a new PoW Captcha record to the database.
429
+ * @param {string} challenge The challenge string for the captcha.
430
+ * @param components The components of the PoW challenge.
431
+ * @param difficulty
432
+ * @param providerSignature
433
+ * @param ipAddress
434
+ * @param headers
435
+ * @param ja4
436
+ * @param frictionlessTokenId
437
+ * @param serverChecked
438
+ * @param userSubmitted
439
+ * @param storedStatus
440
+ * @param userSignature
441
+ * @returns {Promise<void>} A promise that resolves when the record is added.
442
+ */
443
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, frictionlessTokenId, serverChecked = false, userSubmitted = false, storedStatus = StoredStatusNames.notStored, userSignature) {
444
+ const tables = this.getTables();
445
+ const powCaptchaRecord = {
446
+ challenge,
447
+ ...components,
448
+ ipAddress,
449
+ headers,
450
+ ja4,
451
+ result: { status: CaptchaStatus.pending },
452
+ userSubmitted,
453
+ serverChecked,
454
+ difficulty,
455
+ providerSignature,
456
+ userSignature,
457
+ lastUpdatedTimestamp: Date.now(),
458
+ frictionlessTokenId
459
+ };
460
+ try {
461
+ await tables.powcaptcha.create(powCaptchaRecord);
462
+ this.logger.info(() => ({
463
+ data: {
464
+ challenge,
465
+ userSubmitted,
466
+ serverChecked,
467
+ storedStatus
468
+ },
469
+ msg: "PowCaptcha record added successfully"
470
+ }));
471
+ } catch (error) {
472
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
473
+ context: {
474
+ error,
475
+ challenge,
476
+ userSubmitted,
477
+ serverChecked,
478
+ storedStatus
479
+ },
480
+ logger: this.logger
481
+ });
482
+ this.logger.error(() => ({
483
+ err: error,
484
+ msg: "Failed to add PowCaptcha record"
485
+ }));
486
+ throw err;
603
487
  }
604
- async markDappUserPoWCommitmentsStored(challenges) {
605
- const updateDoc = {
606
- storedAtTimestamp: Date.now(),
607
- };
608
- await this.tables?.powcaptcha.updateMany({ challenge: { $in: challenges } }, { $set: updateDoc }, { upsert: false });
488
+ }
489
+ /**
490
+ * @description Retrieves a PoW Captcha record by its challenge string.
491
+ * @param {string} challenge The challenge string to search for.
492
+ * @returns {Promise<PoWCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
493
+ */
494
+ async getPowCaptchaRecordByChallenge(challenge) {
495
+ if (!this.tables) {
496
+ throw new ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
497
+ context: { failedFuncName: this.getPowCaptchaRecordByChallenge.name },
498
+ logger: this.logger
499
+ });
609
500
  }
610
- async markDappUserPoWCommitmentsChecked(challenges) {
611
- const updateDoc = {
612
- [StoredStatusNames.serverChecked]: true,
613
- lastUpdatedTimestamp: Date.now(),
614
- };
615
- await this.tables?.powcaptcha.updateMany({ challenge: { $in: challenges } }, {
616
- $set: updateDoc,
617
- }, { upsert: false });
618
- }
619
- async storeFrictionlessTokenRecord(tokenRecord) {
620
- const doc = await this.tables.frictionlessToken.create(tokenRecord);
621
- return doc._id;
622
- }
623
- async updateFrictionlessTokenRecord(tokenId, updates) {
624
- const filter = { _id: tokenId };
625
- await this.tables.frictionlessToken.updateOne(filter, updates);
626
- }
627
- async getFrictionlessTokenRecordByTokenId(tokenId) {
628
- const filter = { _id: tokenId };
629
- const doc = await this.tables.frictionlessToken.findOne(filter);
630
- return doc ? doc : undefined;
631
- }
632
- async getFrictionlessTokenRecordsByTokenIds(tokenId) {
633
- const filter = {
634
- _id: { $in: tokenId },
635
- };
636
- return this.tables.frictionlessToken.find(filter);
637
- }
638
- async getFrictionlessTokenRecordByToken(token) {
639
- const filter = { token };
640
- const record = await this.tables.frictionlessToken.findOne(filter);
641
- return record || undefined;
642
- }
643
- async storeSessionRecord(sessionRecord) {
644
- try {
645
- this.logger.debug(() => ({
646
- data: { action: "storing", sessionRecord },
647
- }));
648
- await this.tables.session.create(sessionRecord);
649
- }
650
- catch (err) {
651
- throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
652
- context: { error: err, sessionId: sessionRecord.sessionId },
653
- logger: this.logger,
654
- });
655
- }
656
- }
657
- async checkAndRemoveSession(sessionId) {
658
- this.logger.debug(() => ({
659
- data: { action: "checking and removing", sessionId },
501
+ try {
502
+ const filter = { challenge };
503
+ const record = await this.tables.powcaptcha.findOne(filter).lean();
504
+ if (record) {
505
+ this.logger.info(() => ({
506
+ data: { challenge },
507
+ msg: "PowCaptcha record retrieved successfully"
660
508
  }));
661
- const filter = {
662
- sessionId,
663
- deleted: { $exists: false },
664
- };
665
- try {
666
- const session = await this.tables.session
667
- .findOneAndUpdate(filter, {
668
- deleted: true,
669
- lastUpdatedTimestamp: Date.now(),
670
- })
671
- .lean();
672
- return session || undefined;
673
- }
674
- catch (err) {
675
- throw new ProsopoDBError("DATABASE.SESSION_CHECK_REMOVE_FAILED", {
676
- context: { error: err, sessionId },
677
- logger: this.logger,
678
- });
679
- }
509
+ return record;
510
+ }
511
+ this.logger.info(() => ({
512
+ data: { challenge },
513
+ msg: "No PowCaptcha record found"
514
+ }));
515
+ return null;
516
+ } catch (error) {
517
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
518
+ context: { error, challenge },
519
+ logger: this.logger
520
+ });
521
+ this.logger.error(() => ({
522
+ err,
523
+ msg: "Failed to retrieve PowCaptcha record"
524
+ }));
525
+ throw err;
680
526
  }
681
- getUnstoredSessionRecords(limit = 1000, skip = 0) {
682
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
683
- return this.tables?.session
684
- .aggregate([
685
- {
686
- $match: {
687
- $or: [
688
- filterNoStoredTimestamp,
689
- {
690
- $expr: {
691
- $lt: [
692
- {
693
- $convert: {
694
- input: "$storedAtTimestamp",
695
- to: "date",
696
- },
697
- },
698
- {
699
- $convert: {
700
- input: "$lastUpdatedTimestamp",
701
- to: "date",
702
- },
703
- },
704
- ],
705
- },
706
- },
707
- ],
708
- },
709
- },
710
- {
711
- $sort: { _id: 1 },
712
- },
713
- {
714
- $skip: skip,
715
- },
716
- {
717
- $limit: limit,
718
- },
719
- ])
720
- .then((docs) => docs || []);
721
- }
722
- async markSessionRecordsStored(sessionIds) {
723
- const updateDoc = {
724
- storedAtTimestamp: Date.now(),
725
- };
726
- await this.tables?.session.updateMany({ sessionId: { $in: sessionIds } }, { $set: updateDoc }, { upsert: false });
727
- }
728
- async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, frictionlessTokenId) {
729
- if (!isHex(requestHash)) {
730
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
731
- context: {
732
- failedFuncName: this.storePendingImageCommitment.name,
733
- requestHash,
734
- },
735
- });
736
- }
737
- const pendingRecord = {
738
- accountId: userAccount,
739
- pending: true,
740
- salt,
741
- requestHash,
742
- deadlineTimestamp,
743
- requestedAtTimestamp: new Date(requestedAtTimestamp),
744
- ipAddress,
745
- frictionlessTokenId,
746
- threshold,
747
- };
748
- await this.tables?.pending.updateOne({ requestHash: requestHash }, { $set: pendingRecord }, { upsert: true });
749
- }
750
- async getPendingImageCommitment(requestHash) {
751
- if (!isHex(requestHash)) {
752
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
753
- context: {
754
- failedFuncName: this.getPendingImageCommitment.name,
755
- requestHash,
756
- },
757
- });
758
- }
759
- const filter = {
760
- [ApiParams.requestHash]: requestHash,
761
- };
762
- const doc = await this.tables?.pending.findOne(filter).lean();
763
- if (doc) {
764
- return doc;
527
+ }
528
+ /**
529
+ * @description Updates a PoW Captcha record in the database.
530
+ * @param {string} challenge The challenge string of the captcha to be updated.
531
+ * @param result
532
+ * @param serverChecked
533
+ * @param userSubmitted
534
+ * @param userSignature
535
+ * @returns {Promise<void>} A promise that resolves when the record is updated.
536
+ */
537
+ async updatePowCaptchaRecord(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
538
+ const tables = this.getTables();
539
+ const timestamp = Date.now();
540
+ const update = {
541
+ result,
542
+ serverChecked,
543
+ userSubmitted,
544
+ userSignature,
545
+ lastUpdatedTimestamp: timestamp
546
+ };
547
+ try {
548
+ const updateResult = await tables.powcaptcha.updateOne(
549
+ { challenge },
550
+ {
551
+ $set: update
765
552
  }
766
- throw new ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
767
- context: {
768
- failedFuncName: this.getPendingImageCommitment.name,
769
- requestHash,
770
- },
553
+ );
554
+ if (updateResult.matchedCount === 0) {
555
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
556
+ context: {
557
+ challenge,
558
+ ...update
559
+ },
560
+ logger: this.logger
771
561
  });
562
+ this.logger.info(() => ({
563
+ err,
564
+ msg: "No PowCaptcha record found to update"
565
+ }));
566
+ throw err;
567
+ }
568
+ this.logger.info(() => ({
569
+ data: {
570
+ challenge,
571
+ ...update
572
+ },
573
+ msg: "PowCaptcha record updated successfully"
574
+ }));
575
+ } catch (error) {
576
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
577
+ context: {
578
+ error,
579
+ challenge,
580
+ ...update
581
+ },
582
+ logger: this.logger
583
+ });
584
+ this.logger.error(() => ({
585
+ err,
586
+ msg: "Failed to update PowCaptcha record"
587
+ }));
588
+ throw err;
772
589
  }
773
- async updatePendingImageCommitmentStatus(requestHash) {
774
- if (!isHex(requestHash)) {
775
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
776
- context: {
777
- failedFuncName: this.updatePendingImageCommitmentStatus.name,
778
- requestHash,
779
- },
780
- });
590
+ }
591
+ /** @description Get serverChecked Dapp User image captcha commitments from the commitments table
592
+ */
593
+ async getCheckedDappUserCommitments() {
594
+ const filter = { [StoredStatusNames.serverChecked]: true };
595
+ const docs = await this.tables?.commitment.find(filter).lean();
596
+ return docs || [];
597
+ }
598
+ /** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
599
+ * client's total
600
+ */
601
+ async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
602
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
603
+ const docs = await this.tables?.commitment.aggregate([
604
+ {
605
+ $match: {
606
+ $or: [
607
+ filterNoStoredTimestamp,
608
+ {
609
+ $expr: {
610
+ $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
611
+ }
612
+ }
613
+ ]
781
614
  }
782
- const filter = {
783
- [ApiParams.requestHash]: requestHash,
784
- };
785
- await this.tables?.pending.updateOne(filter, {
786
- $set: {
787
- [CaptchaStatus.pending]: false,
788
- },
789
- }, { upsert: true });
790
- }
791
- async getAllCaptchasByDatasetId(datasetId, state) {
792
- const filter = {
793
- datasetId,
794
- solved: state === CaptchaStates.Solved,
795
- };
796
- const cursor = this.tables?.captcha
797
- .find(filter)
798
- .lean();
799
- const docs = await cursor;
800
- if (docs) {
801
- return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
615
+ },
616
+ {
617
+ $sort: { _id: 1 }
618
+ },
619
+ {
620
+ $skip: skip
621
+ },
622
+ {
623
+ $limit: limit
624
+ }
625
+ ]);
626
+ return docs || [];
627
+ }
628
+ /** @description Mark a list of captcha commits as stored
629
+ */
630
+ async markDappUserCommitmentsStored(commitmentIds) {
631
+ const updateDoc = {
632
+ storedAtTimestamp: Date.now()
633
+ };
634
+ await this.tables?.commitment.updateMany(
635
+ { id: { $in: commitmentIds } },
636
+ { $set: updateDoc },
637
+ { upsert: false }
638
+ );
639
+ }
640
+ /** @description Mark a list of captcha commits as checked
641
+ */
642
+ async markDappUserCommitmentsChecked(commitmentIds) {
643
+ const updateDoc = {
644
+ [StoredStatusNames.serverChecked]: true,
645
+ lastUpdatedTimestamp: Date.now()
646
+ };
647
+ await this.tables?.commitment.updateMany(
648
+ { id: { $in: commitmentIds } },
649
+ { $set: updateDoc },
650
+ { upsert: false }
651
+ );
652
+ }
653
+ /**
654
+ * @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
655
+ * @param {number} limit Maximum number of records to return
656
+ * @param {number} skip Number of records to skip (for pagination)
657
+ * @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
658
+ */
659
+ async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
660
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
661
+ const docs = await this.tables?.powcaptcha.aggregate([
662
+ {
663
+ $match: {
664
+ $or: [
665
+ filterNoStoredTimestamp,
666
+ {
667
+ $expr: {
668
+ $lt: [
669
+ {
670
+ $convert: {
671
+ input: "$storedAtTimestamp",
672
+ to: "date"
673
+ }
674
+ },
675
+ {
676
+ $convert: {
677
+ input: "$lastUpdatedTimestamp",
678
+ to: "date"
679
+ }
680
+ }
681
+ ]
682
+ }
683
+ }
684
+ ]
802
685
  }
803
- throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED");
686
+ },
687
+ {
688
+ $sort: { _id: 1 }
689
+ },
690
+ {
691
+ $skip: skip
692
+ },
693
+ {
694
+ $limit: limit
695
+ }
696
+ ]);
697
+ return docs || [];
698
+ }
699
+ /** @description Mark a list of PoW captcha commits as stored
700
+ */
701
+ async markDappUserPoWCommitmentsStored(challenges) {
702
+ const updateDoc = {
703
+ storedAtTimestamp: Date.now()
704
+ };
705
+ await this.tables?.powcaptcha.updateMany(
706
+ { challenge: { $in: challenges } },
707
+ { $set: updateDoc },
708
+ { upsert: false }
709
+ );
710
+ }
711
+ /** @description Mark a list of PoW captcha commits as checked by the server
712
+ */
713
+ async markDappUserPoWCommitmentsChecked(challenges) {
714
+ const updateDoc = {
715
+ [StoredStatusNames.serverChecked]: true,
716
+ lastUpdatedTimestamp: Date.now()
717
+ };
718
+ await this.tables?.powcaptcha.updateMany(
719
+ { challenge: { $in: challenges } },
720
+ {
721
+ $set: updateDoc
722
+ },
723
+ { upsert: false }
724
+ );
725
+ }
726
+ /**
727
+ * Store a new frictionless token record
728
+ */
729
+ async storeFrictionlessTokenRecord(tokenRecord) {
730
+ const doc = await this.tables.frictionlessToken.create(
731
+ tokenRecord
732
+ );
733
+ return doc._id;
734
+ }
735
+ /** Update a frictionless token record */
736
+ async updateFrictionlessTokenRecord(tokenId, updates) {
737
+ const filter = { _id: tokenId };
738
+ await this.tables.frictionlessToken.updateOne(filter, updates);
739
+ }
740
+ /** Get a frictionless token record */
741
+ async getFrictionlessTokenRecordByTokenId(tokenId) {
742
+ const filter = { _id: tokenId };
743
+ const doc = await this.tables.frictionlessToken.findOne(
744
+ filter
745
+ );
746
+ return doc ? doc : void 0;
747
+ }
748
+ /** Get many frictionless token records */
749
+ async getFrictionlessTokenRecordsByTokenIds(tokenId) {
750
+ const filter = {
751
+ _id: { $in: tokenId }
752
+ };
753
+ return this.tables.frictionlessToken.find(filter);
754
+ }
755
+ /**
756
+ * Check if a frictionless token record exists.
757
+ * Used to ensure that a token is not used more than once.
758
+ */
759
+ async getFrictionlessTokenRecordByToken(token) {
760
+ const filter = { token };
761
+ const record = await this.tables.frictionlessToken.findOne(
762
+ filter
763
+ );
764
+ return record || void 0;
765
+ }
766
+ /**
767
+ * Store a new session record
768
+ */
769
+ async storeSessionRecord(sessionRecord) {
770
+ try {
771
+ this.logger.debug(() => ({
772
+ data: { action: "storing", sessionRecord }
773
+ }));
774
+ await this.tables.session.create(sessionRecord);
775
+ } catch (err) {
776
+ throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
777
+ context: { error: err, sessionId: sessionRecord.sessionId },
778
+ logger: this.logger
779
+ });
804
780
  }
805
- async getAllDappUserSolutions(captchaId) {
806
- const filter = {
807
- captchaId: { $in: captchaId },
808
- };
809
- const cursor = this.tables?.usersolution
810
- ?.find(filter)
811
- .lean();
812
- const docs = await cursor;
813
- if (docs) {
814
- return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
815
- }
816
- throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED");
781
+ }
782
+ /**
783
+ * Check if a session exists and mark it as removed
784
+ * @returns The session record if it existed, undefined otherwise
785
+ */
786
+ async checkAndRemoveSession(sessionId) {
787
+ this.logger.debug(() => ({
788
+ data: { action: "checking and removing", sessionId }
789
+ }));
790
+ const filter = {
791
+ sessionId,
792
+ deleted: { $exists: false }
793
+ };
794
+ try {
795
+ const session = await this.tables.session.findOneAndUpdate(filter, {
796
+ deleted: true,
797
+ lastUpdatedTimestamp: Date.now()
798
+ }).lean();
799
+ return session || void 0;
800
+ } catch (err) {
801
+ throw new ProsopoDBError("DATABASE.SESSION_CHECK_REMOVE_FAILED", {
802
+ context: { error: err, sessionId },
803
+ logger: this.logger
804
+ });
817
805
  }
818
- async getDatasetIdWithSolvedCaptchasOfSizeN(solvedCaptchaCount) {
819
- const cursor = this.tables?.solution.aggregate([
820
- {
821
- $match: {},
822
- },
806
+ }
807
+ /** Get unstored session records
808
+ * @description Get session records that have not been stored yet
809
+ * @param limit
810
+ * @param skip
811
+ */
812
+ getUnstoredSessionRecords(limit = 1e3, skip = 0) {
813
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
814
+ return this.tables?.session.aggregate([
815
+ {
816
+ $match: {
817
+ $or: [
818
+ filterNoStoredTimestamp,
823
819
  {
824
- $group: {
825
- _id: "$datasetId",
826
- count: { $sum: 1 },
827
- },
828
- },
829
- {
830
- $match: {
831
- count: { $gte: solvedCaptchaCount },
832
- },
833
- },
834
- {
835
- $sample: { size: 1 },
836
- },
837
- ]);
838
- const docs = await cursor;
839
- if (docs?.length) {
840
- return docs[0]._id;
841
- }
842
- throw new ProsopoDBError("DATABASE.DATASET_WITH_SOLUTIONS_GET_FAILED");
843
- }
844
- async getRandomSolvedCaptchasFromSingleDataset(datasetId, size) {
845
- if (!isHex(datasetId)) {
846
- throw new ProsopoDBError("DATABASE.INVALID_HASH", {
847
- context: {
848
- failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
849
- datasetId,
850
- },
851
- });
820
+ $expr: {
821
+ $lt: [
822
+ {
823
+ $convert: {
824
+ input: "$storedAtTimestamp",
825
+ to: "date"
826
+ }
827
+ },
828
+ {
829
+ $convert: {
830
+ input: "$lastUpdatedTimestamp",
831
+ to: "date"
832
+ }
833
+ }
834
+ ]
835
+ }
836
+ }
837
+ ]
852
838
  }
853
- const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
854
- const cursor = this.tables?.solution.aggregate([
855
- { $match: { datasetId } },
856
- { $sample: { size: sampleSize } },
857
- {
858
- $project: {
859
- captchaId: 1,
860
- captchaContentId: 1,
861
- solution: 1,
862
- },
863
- },
864
- ]);
865
- const docs = await cursor;
866
- if (docs?.length) {
867
- return docs;
839
+ },
840
+ {
841
+ $sort: { _id: 1 }
842
+ },
843
+ {
844
+ $skip: skip
845
+ },
846
+ {
847
+ $limit: limit
848
+ }
849
+ ]).then((docs) => docs || []);
850
+ }
851
+ /** Mark a list of session records as stored */
852
+ async markSessionRecordsStored(sessionIds) {
853
+ const updateDoc = {
854
+ storedAtTimestamp: Date.now()
855
+ };
856
+ await this.tables?.session.updateMany(
857
+ { sessionId: { $in: sessionIds } },
858
+ { $set: updateDoc },
859
+ { upsert: false }
860
+ );
861
+ }
862
+ /**
863
+ * @description Store a Dapp User's pending record
864
+ */
865
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, frictionlessTokenId) {
866
+ if (!isHex(requestHash)) {
867
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
868
+ context: {
869
+ failedFuncName: this.storePendingImageCommitment.name,
870
+ requestHash
868
871
  }
869
- throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
870
- context: {
871
- failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
872
- datasetId,
873
- size,
874
- },
875
- });
872
+ });
876
873
  }
877
- async getDappUserSolutionById(commitmentId) {
878
- const filter = {
879
- commitmentId: commitmentId,
880
- };
881
- const project = { projection: { _id: 0 } };
882
- const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
883
- const doc = await cursor;
884
- if (doc) {
885
- return doc;
874
+ const pendingRecord = {
875
+ accountId: userAccount,
876
+ pending: true,
877
+ salt,
878
+ requestHash,
879
+ deadlineTimestamp,
880
+ requestedAtTimestamp: new Date(requestedAtTimestamp),
881
+ ipAddress,
882
+ frictionlessTokenId,
883
+ threshold
884
+ };
885
+ await this.tables?.pending.updateOne(
886
+ { requestHash },
887
+ { $set: pendingRecord },
888
+ { upsert: true }
889
+ );
890
+ }
891
+ /**
892
+ * @description Get a Dapp user's pending record
893
+ */
894
+ async getPendingImageCommitment(requestHash) {
895
+ if (!isHex(requestHash)) {
896
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
897
+ context: {
898
+ failedFuncName: this.getPendingImageCommitment.name,
899
+ requestHash
886
900
  }
887
- throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
888
- context: { failedFuncName: this.getCaptchaById.name, commitmentId },
889
- });
890
- }
891
- async getDappUserCommitmentById(commitmentId) {
892
- const filter = { id: commitmentId };
893
- const commitmentCursor = this.tables?.commitment
894
- ?.findOne(filter)
895
- .lean();
896
- const doc = await commitmentCursor;
897
- return doc ? doc : undefined;
901
+ });
898
902
  }
899
- async getDappUserCommitmentByAccount(userAccount, dappAccount) {
900
- const filter = {
901
- userAccount,
902
- dappAccount,
903
- };
904
- const project = { _id: 0 };
905
- const sort = { sort: { _id: -1 } };
906
- const docs = await this.tables?.commitment
907
- ?.find(filter, project, sort)
908
- .lean();
909
- return docs ? docs : [];
903
+ const filter = {
904
+ [ApiParams.requestHash]: requestHash
905
+ };
906
+ const doc = await this.tables?.pending.findOne(filter).lean();
907
+ if (doc) {
908
+ return doc;
910
909
  }
911
- async approveDappUserCommitment(commitmentId) {
912
- try {
913
- const result = { status: CaptchaStatus.approved };
914
- const updateDoc = {
915
- result,
916
- lastUpdatedTimestamp: Date.now(),
917
- };
918
- const filter = { id: commitmentId };
919
- await this.tables?.commitment
920
- ?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false })
921
- .lean();
922
- }
923
- catch (err) {
924
- throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
925
- context: { error: err, commitmentId },
926
- });
910
+ throw new ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
911
+ context: {
912
+ failedFuncName: this.getPendingImageCommitment.name,
913
+ requestHash
914
+ }
915
+ });
916
+ }
917
+ /**
918
+ * @description Mark a pending request as used
919
+ */
920
+ async updatePendingImageCommitmentStatus(requestHash) {
921
+ if (!isHex(requestHash)) {
922
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
923
+ context: {
924
+ failedFuncName: this.updatePendingImageCommitmentStatus.name,
925
+ requestHash
927
926
  }
927
+ });
928
928
  }
929
- async disapproveDappUserCommitment(commitmentId, reason) {
930
- try {
931
- const updateDoc = {
932
- result: { status: CaptchaStatus.disapproved, reason },
933
- lastUpdatedTimestamp: Date.now(),
934
- };
935
- const filter = { id: commitmentId };
936
- await this.tables?.commitment
937
- ?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false })
938
- .lean();
939
- }
940
- catch (err) {
941
- throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
942
- context: { error: err, commitmentId },
943
- });
929
+ const filter = {
930
+ [ApiParams.requestHash]: requestHash
931
+ };
932
+ await this.tables?.pending.updateOne(
933
+ filter,
934
+ {
935
+ $set: {
936
+ [CaptchaStatus.pending]: false
944
937
  }
938
+ },
939
+ { upsert: true }
940
+ );
941
+ }
942
+ /**
943
+ * @description Get all unsolved captchas
944
+ */
945
+ async getAllCaptchasByDatasetId(datasetId, state) {
946
+ const filter = {
947
+ datasetId,
948
+ solved: state === CaptchaStates.Solved
949
+ };
950
+ const cursor = this.tables?.captcha.find(filter).lean();
951
+ const docs = await cursor;
952
+ if (docs) {
953
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
945
954
  }
946
- async flagProcessedDappUserSolutions(captchaIds) {
947
- try {
948
- await this.tables?.usersolution
949
- ?.updateMany({ captchaId: { $in: captchaIds } }, { $set: { processed: true } }, { upsert: false })
950
- .lean();
951
- }
952
- catch (err) {
953
- throw new ProsopoDBError("DATABASE.SOLUTION_FLAG_FAILED", {
954
- context: { error: err, captchaIds },
955
- });
956
- }
955
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED");
956
+ }
957
+ /**
958
+ * @description Get all dapp user solutions by captchaIds
959
+ */
960
+ async getAllDappUserSolutions(captchaId) {
961
+ const filter = {
962
+ captchaId: { $in: captchaId }
963
+ };
964
+ const cursor = this.tables?.usersolution?.find(filter).lean();
965
+ const docs = await cursor;
966
+ if (docs) {
967
+ return docs.map(
968
+ ({ _id, ...keepAttrs }) => keepAttrs
969
+ );
957
970
  }
958
- async flagProcessedDappUserCommitments(commitmentIds) {
959
- try {
960
- const distinctCommitmentIds = [...new Set(commitmentIds)];
961
- await this.tables?.commitment
962
- ?.updateMany({ id: { $in: distinctCommitmentIds } }, { $set: { processed: true } }, { upsert: false })
963
- .lean();
971
+ throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED");
972
+ }
973
+ async getDatasetIdWithSolvedCaptchasOfSizeN(solvedCaptchaCount) {
974
+ const cursor = this.tables?.solution.aggregate([
975
+ {
976
+ $match: {}
977
+ },
978
+ {
979
+ $group: {
980
+ _id: "$datasetId",
981
+ count: { $sum: 1 }
964
982
  }
965
- catch (err) {
966
- throw new ProsopoDBError("DATABASE.COMMITMENT_FLAG_FAILED", {
967
- context: { error: err, commitmentIds },
968
- });
983
+ },
984
+ {
985
+ $match: {
986
+ count: { $gte: solvedCaptchaCount }
969
987
  }
988
+ },
989
+ {
990
+ $sample: { size: 1 }
991
+ }
992
+ ]);
993
+ const docs = await cursor;
994
+ if (docs?.length) {
995
+ return docs[0]._id;
970
996
  }
971
- async getScheduledTaskStatus(taskId, status) {
972
- const filter = {
973
- _id: taskId,
974
- status: status,
975
- };
976
- const cursor = await this.tables?.scheduler
977
- ?.findOne(filter)
978
- .lean();
979
- return cursor ? cursor : undefined;
980
- }
981
- async getLastScheduledTaskStatus(task, status) {
982
- const filter = { processName: task };
983
- if (status) {
984
- filter.status = status;
997
+ throw new ProsopoDBError("DATABASE.DATASET_WITH_SOLUTIONS_GET_FAILED");
998
+ }
999
+ async getRandomSolvedCaptchasFromSingleDataset(datasetId, size) {
1000
+ if (!isHex(datasetId)) {
1001
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
1002
+ context: {
1003
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
1004
+ datasetId
985
1005
  }
986
- const sort = {
987
- datetime: -1,
988
- };
989
- const cursor = await this.tables?.scheduler
990
- ?.findOne(filter)
991
- .sort(sort)
992
- .limit(1)
993
- .lean();
994
- return cursor ? cursor : undefined;
995
- }
996
- async createScheduledTaskStatus(taskName, status) {
997
- const now = new Date().getTime();
998
- const doc = ScheduledTaskSchema.parse({
999
- processName: taskName,
1000
- datetime: now,
1001
- status,
1002
- });
1003
- const taskRecord = await this.tables?.scheduler.create(doc);
1004
- return taskRecord._id;
1006
+ });
1005
1007
  }
1006
- async updateScheduledTaskStatus(taskId, status, result) {
1007
- const update = {
1008
- status,
1009
- updated: new Date().getTime(),
1010
- ...(result && { result }),
1011
- };
1012
- const filter = { _id: taskId };
1013
- await this.tables?.scheduler.updateOne(filter, { $set: update }, {
1014
- upsert: false,
1015
- });
1008
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
1009
+ const cursor = this.tables?.solution.aggregate([
1010
+ { $match: { datasetId } },
1011
+ { $sample: { size: sampleSize } },
1012
+ {
1013
+ $project: {
1014
+ captchaId: 1,
1015
+ captchaContentId: 1,
1016
+ solution: 1
1017
+ }
1018
+ }
1019
+ ]);
1020
+ const docs = await cursor;
1021
+ if (docs?.length) {
1022
+ return docs;
1016
1023
  }
1017
- async cleanupScheduledTaskStatus(status) {
1018
- const filter = {
1019
- status,
1020
- };
1021
- await this.tables?.scheduler.deleteMany(filter);
1024
+ throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
1025
+ context: {
1026
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
1027
+ datasetId,
1028
+ size
1029
+ }
1030
+ });
1031
+ }
1032
+ /**
1033
+ * @description Get dapp user solution by ID
1034
+ * @param {string[]} commitmentId
1035
+ */
1036
+ async getDappUserSolutionById(commitmentId) {
1037
+ const filter = {
1038
+ commitmentId
1039
+ };
1040
+ const project = { projection: { _id: 0 } };
1041
+ const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
1042
+ const doc = await cursor;
1043
+ if (doc) {
1044
+ return doc;
1022
1045
  }
1023
- async updateClientRecords(clientRecords) {
1024
- const ops = clientRecords.map((record) => {
1025
- const clientRecord = {
1026
- account: record.account,
1027
- settings: record.settings,
1028
- tier: record.tier,
1029
- };
1030
- const filter = {
1031
- account: record.account,
1032
- };
1033
- return {
1034
- updateOne: {
1035
- filter,
1036
- update: {
1037
- $set: clientRecord,
1038
- },
1039
- upsert: true,
1040
- },
1041
- };
1042
- });
1043
- await this.tables?.client.bulkWrite(ops);
1046
+ throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
1047
+ context: { failedFuncName: this.getCaptchaById.name, commitmentId }
1048
+ });
1049
+ }
1050
+ /**
1051
+ * @description Get dapp user commitment by user account
1052
+ * @param commitmentId
1053
+ */
1054
+ async getDappUserCommitmentById(commitmentId) {
1055
+ const filter = { id: commitmentId };
1056
+ const commitmentCursor = this.tables?.commitment?.findOne(filter).lean();
1057
+ const doc = await commitmentCursor;
1058
+ return doc ? doc : void 0;
1059
+ }
1060
+ /**
1061
+ * @description Get dapp user commitment by user account
1062
+ * @param {string} userAccount
1063
+ * @param {string} dappAccount
1064
+ */
1065
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
1066
+ const filter = {
1067
+ userAccount,
1068
+ dappAccount
1069
+ };
1070
+ const project = { _id: 0 };
1071
+ const sort = { sort: { _id: -1 } };
1072
+ const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
1073
+ return docs ? docs : [];
1074
+ }
1075
+ /**
1076
+ * @description Approve a dapp user's solution
1077
+ * @param {string[]} commitmentId
1078
+ */
1079
+ async approveDappUserCommitment(commitmentId) {
1080
+ try {
1081
+ const result = { status: CaptchaStatus.approved };
1082
+ const updateDoc = {
1083
+ result,
1084
+ lastUpdatedTimestamp: Date.now()
1085
+ };
1086
+ const filter = { id: commitmentId };
1087
+ await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1088
+ } catch (err) {
1089
+ throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1090
+ context: { error: err, commitmentId }
1091
+ });
1044
1092
  }
1045
- async getClientRecord(account) {
1046
- const filter = { account };
1047
- const doc = await this.tables?.client.findOne(filter).lean();
1048
- return doc ? doc : undefined;
1093
+ }
1094
+ /**
1095
+ * @description Disapprove a dapp user's solution
1096
+ * @param {string} commitmentId
1097
+ * @param reason
1098
+ */
1099
+ async disapproveDappUserCommitment(commitmentId, reason) {
1100
+ try {
1101
+ const updateDoc = {
1102
+ result: { status: CaptchaStatus.disapproved, reason },
1103
+ lastUpdatedTimestamp: Date.now()
1104
+ };
1105
+ const filter = { id: commitmentId };
1106
+ await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1107
+ } catch (err) {
1108
+ throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1109
+ context: { error: err, commitmentId }
1110
+ });
1049
1111
  }
1050
- async storeDetectorKey(detectorKey) {
1051
- return this.tables?.detector.create({
1052
- detectorKey,
1053
- createdAt: new Date(),
1054
- });
1112
+ }
1113
+ /**
1114
+ * @description Flag a dapp user's solutions as used by calculated solution
1115
+ * @param {string[]} captchaIds
1116
+ */
1117
+ async flagProcessedDappUserSolutions(captchaIds) {
1118
+ try {
1119
+ await this.tables?.usersolution?.updateMany(
1120
+ { captchaId: { $in: captchaIds } },
1121
+ { $set: { processed: true } },
1122
+ { upsert: false }
1123
+ ).lean();
1124
+ } catch (err) {
1125
+ throw new ProsopoDBError("DATABASE.SOLUTION_FLAG_FAILED", {
1126
+ context: { error: err, captchaIds }
1127
+ });
1055
1128
  }
1056
- async removeDetectorKey(detectorKey) {
1057
- const filter = { detectorKey };
1058
- const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
1059
- await this.tables?.detector.updateOne(filter, {
1060
- $set: { expiresAt },
1061
- });
1129
+ }
1130
+ /**
1131
+ * @description Flag dapp users' commitments as used by calculated solution
1132
+ * @param {string[]} commitmentIds
1133
+ */
1134
+ async flagProcessedDappUserCommitments(commitmentIds) {
1135
+ try {
1136
+ const distinctCommitmentIds = [...new Set(commitmentIds)];
1137
+ await this.tables?.commitment?.updateMany(
1138
+ { id: { $in: distinctCommitmentIds } },
1139
+ { $set: { processed: true } },
1140
+ { upsert: false }
1141
+ ).lean();
1142
+ } catch (err) {
1143
+ throw new ProsopoDBError("DATABASE.COMMITMENT_FLAG_FAILED", {
1144
+ context: { error: err, commitmentIds }
1145
+ });
1062
1146
  }
1063
- async getDetectorKeys() {
1064
- const keyRecords = await this.tables?.detector
1065
- .find({
1066
- $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }],
1067
- }, { detectorKey: 1 })
1068
- .sort({ createdAt: -1 })
1069
- .lean();
1070
- return (keyRecords || []).map((record) => record.detectorKey);
1147
+ }
1148
+ /**
1149
+ * @description Get a scheduled task status record by task ID and status
1150
+ */
1151
+ async getScheduledTaskStatus(taskId, status) {
1152
+ const filter = {
1153
+ _id: taskId,
1154
+ status
1155
+ };
1156
+ const cursor = await this.tables?.scheduler?.findOne(filter).lean();
1157
+ return cursor ? cursor : void 0;
1158
+ }
1159
+ /**
1160
+ * @description Get the most recent scheduled task status record for a given task
1161
+ */
1162
+ async getLastScheduledTaskStatus(task, status) {
1163
+ const filter = { processName: task };
1164
+ if (status) {
1165
+ filter.status = status;
1071
1166
  }
1167
+ const sort = {
1168
+ datetime: -1
1169
+ };
1170
+ const cursor = await this.tables?.scheduler?.findOne(filter).sort(sort).limit(1).lean();
1171
+ return cursor ? cursor : void 0;
1172
+ }
1173
+ /**
1174
+ * @description Create the status of a scheduled task
1175
+ */
1176
+ async createScheduledTaskStatus(taskName, status) {
1177
+ const now = (/* @__PURE__ */ new Date()).getTime();
1178
+ const doc = ScheduledTaskSchema.parse({
1179
+ processName: taskName,
1180
+ datetime: now,
1181
+ status
1182
+ });
1183
+ const taskRecord = await this.tables?.scheduler.create(doc);
1184
+ return taskRecord._id;
1185
+ }
1186
+ /**
1187
+ * @description Update the status of a scheduled task and an optional result
1188
+ */
1189
+ async updateScheduledTaskStatus(taskId, status, result) {
1190
+ const update = {
1191
+ status,
1192
+ updated: (/* @__PURE__ */ new Date()).getTime(),
1193
+ ...result && { result }
1194
+ };
1195
+ const filter = { _id: taskId };
1196
+ await this.tables?.scheduler.updateOne(
1197
+ filter,
1198
+ { $set: update },
1199
+ {
1200
+ upsert: false
1201
+ }
1202
+ );
1203
+ }
1204
+ /**
1205
+ * @description Clean up the scheduled task status records
1206
+ */
1207
+ async cleanupScheduledTaskStatus(status) {
1208
+ const filter = {
1209
+ status
1210
+ };
1211
+ await this.tables?.scheduler.deleteMany(filter);
1212
+ }
1213
+ /**
1214
+ * @description Update the client records
1215
+ */
1216
+ async updateClientRecords(clientRecords) {
1217
+ const ops = clientRecords.map((record) => {
1218
+ const clientRecord = {
1219
+ account: record.account,
1220
+ settings: record.settings,
1221
+ tier: record.tier
1222
+ };
1223
+ const filter = {
1224
+ account: record.account
1225
+ };
1226
+ return {
1227
+ updateOne: {
1228
+ filter,
1229
+ update: {
1230
+ $set: clientRecord
1231
+ },
1232
+ upsert: true
1233
+ }
1234
+ };
1235
+ });
1236
+ await this.tables?.client.bulkWrite(ops);
1237
+ }
1238
+ /**
1239
+ * @description Get a client record
1240
+ */
1241
+ async getClientRecord(account) {
1242
+ const filter = { account };
1243
+ const doc = await this.tables?.client.findOne(filter).lean();
1244
+ return doc ? doc : void 0;
1245
+ }
1246
+ /**
1247
+ * @description Store a detector key
1248
+ */
1249
+ async storeDetectorKey(detectorKey) {
1250
+ return this.tables?.detector.create({
1251
+ detectorKey,
1252
+ createdAt: /* @__PURE__ */ new Date()
1253
+ });
1254
+ }
1255
+ /** @description Remove a detector key */
1256
+ async removeDetectorKey(detectorKey) {
1257
+ const filter = { detectorKey };
1258
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
1259
+ await this.tables?.detector.updateOne(filter, {
1260
+ $set: { expiresAt }
1261
+ });
1262
+ }
1263
+ /**
1264
+ * @description Get valid detector keys
1265
+ */
1266
+ async getDetectorKeys() {
1267
+ const keyRecords = await this.tables?.detector.find(
1268
+ {
1269
+ $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }]
1270
+ },
1271
+ { detectorKey: 1 }
1272
+ ).sort({ createdAt: -1 }).lean();
1273
+ return (keyRecords || []).map((record) => record.detectorKey);
1274
+ }
1072
1275
  }
1073
- //# sourceMappingURL=provider.js.map
1276
+ export {
1277
+ ProviderDatabase
1278
+ };