@prosopo/database 2.1.8 → 2.1.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.
@@ -0,0 +1,874 @@
1
+ import { isHex } from "@polkadot/util/is";
2
+ import { ProsopoDBError, ProsopoEnvError } from "@prosopo/common";
3
+ import { CaptchaStates, CaptchaStatus, DatasetWithIdsAndTreeSchema, } from "@prosopo/types";
4
+ import { CaptchaRecordSchema, ClientRecordSchema, DatasetRecordSchema, IPBlockRuleRecordSchema, PendingRecordSchema, PoWCaptchaRecordSchema, ScheduledTaskRecordSchema, ScheduledTaskSchema, SessionRecordSchema, SolutionRecordSchema, StoredStatusNames, UserAccountBlockRuleSchema, UserCommitmentRecordSchema, UserCommitmentSchema, UserSolutionRecordSchema, } from "@prosopo/types-database";
5
+ import { MongoDatabase } from "../base/mongo.js";
6
+ var TableNames;
7
+ (function (TableNames) {
8
+ TableNames["captcha"] = "captcha";
9
+ TableNames["dataset"] = "dataset";
10
+ TableNames["solution"] = "solution";
11
+ TableNames["commitment"] = "commitment";
12
+ TableNames["usersolution"] = "usersolution";
13
+ TableNames["pending"] = "pending";
14
+ TableNames["scheduler"] = "scheduler";
15
+ TableNames["powcaptcha"] = "powcaptcha";
16
+ TableNames["client"] = "client";
17
+ TableNames["session"] = "session";
18
+ TableNames["ipblockrules"] = "ipblockrules";
19
+ TableNames["userblockrules"] = "userblockrules";
20
+ })(TableNames || (TableNames = {}));
21
+ const PROVIDER_TABLES = [
22
+ {
23
+ collectionName: TableNames.captcha,
24
+ modelName: "Captcha",
25
+ schema: CaptchaRecordSchema,
26
+ },
27
+ {
28
+ collectionName: TableNames.powcaptcha,
29
+ modelName: "PowCaptcha",
30
+ schema: PoWCaptchaRecordSchema,
31
+ },
32
+ {
33
+ collectionName: TableNames.dataset,
34
+ modelName: "Dataset",
35
+ schema: DatasetRecordSchema,
36
+ },
37
+ {
38
+ collectionName: TableNames.solution,
39
+ modelName: "Solution",
40
+ schema: SolutionRecordSchema,
41
+ },
42
+ {
43
+ collectionName: TableNames.commitment,
44
+ modelName: "UserCommitment",
45
+ schema: UserCommitmentRecordSchema,
46
+ },
47
+ {
48
+ collectionName: TableNames.usersolution,
49
+ modelName: "UserSolution",
50
+ schema: UserSolutionRecordSchema,
51
+ },
52
+ {
53
+ collectionName: TableNames.pending,
54
+ modelName: "Pending",
55
+ schema: PendingRecordSchema,
56
+ },
57
+ {
58
+ collectionName: TableNames.scheduler,
59
+ modelName: "Scheduler",
60
+ schema: ScheduledTaskRecordSchema,
61
+ },
62
+ {
63
+ collectionName: TableNames.client,
64
+ modelName: "Client",
65
+ schema: ClientRecordSchema,
66
+ },
67
+ {
68
+ collectionName: TableNames.session,
69
+ modelName: "Session",
70
+ schema: SessionRecordSchema,
71
+ },
72
+ {
73
+ collectionName: TableNames.ipblockrules,
74
+ modelName: "IPBlockRules",
75
+ schema: IPBlockRuleRecordSchema,
76
+ },
77
+ {
78
+ collectionName: TableNames.userblockrules,
79
+ modelName: "UserAccountBlockRules",
80
+ schema: UserAccountBlockRuleSchema,
81
+ },
82
+ ];
83
+ export class ProviderDatabase extends MongoDatabase {
84
+ constructor(url, dbname, authSource, logger) {
85
+ super(url, dbname, authSource, logger);
86
+ this.tables = {};
87
+ this.tables = {};
88
+ }
89
+ async connect() {
90
+ await super.connect();
91
+ this.loadTables();
92
+ }
93
+ loadTables() {
94
+ const tables = {};
95
+ PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => {
96
+ if (this.connection) {
97
+ tables[collectionName] = this.connection.model(modelName, schema);
98
+ }
99
+ });
100
+ this.tables = tables;
101
+ }
102
+ getTables() {
103
+ if (!this.tables) {
104
+ throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
105
+ context: { failedFuncName: this.getTables.name },
106
+ logger: this.logger,
107
+ });
108
+ }
109
+ return this.tables;
110
+ }
111
+ async storeDataset(dataset) {
112
+ try {
113
+ this.logger.debug("Storing dataset in database");
114
+ const parsedDataset = DatasetWithIdsAndTreeSchema.parse(dataset);
115
+ const datasetDoc = {
116
+ datasetId: parsedDataset.datasetId,
117
+ datasetContentId: parsedDataset.datasetContentId,
118
+ format: parsedDataset.format,
119
+ contentTree: parsedDataset.contentTree,
120
+ solutionTree: parsedDataset.solutionTree,
121
+ };
122
+ await this.tables.dataset?.updateOne({ datasetId: parsedDataset.datasetId }, { $set: datasetDoc }, { upsert: true });
123
+ const captchaDocs = parsedDataset.captchas.map(({ solution, ...captcha }, index) => ({
124
+ ...captcha,
125
+ datasetId: parsedDataset.datasetId,
126
+ datasetContentId: parsedDataset.datasetContentId,
127
+ index,
128
+ solved: !!solution?.length,
129
+ }));
130
+ this.logger.debug("Inserting captcha records");
131
+ if (captchaDocs.length) {
132
+ await this.tables?.captcha.bulkWrite(captchaDocs.map((captchaDoc) => ({
133
+ updateOne: {
134
+ filter: { captchaId: captchaDoc.captchaId },
135
+ update: { $set: captchaDoc },
136
+ upsert: true,
137
+ },
138
+ })));
139
+ }
140
+ const captchaSolutionDocs = parsedDataset.captchas
141
+ .filter(({ solution }) => solution?.length)
142
+ .map((captcha) => ({
143
+ captchaId: captcha.captchaId,
144
+ captchaContentId: captcha.captchaContentId,
145
+ solution: captcha.solution,
146
+ salt: captcha.salt,
147
+ datasetId: parsedDataset.datasetId,
148
+ datasetContentId: parsedDataset.datasetContentId,
149
+ }));
150
+ this.logger.debug("Inserting solution records");
151
+ if (captchaSolutionDocs.length) {
152
+ await this.tables?.solution.bulkWrite(captchaSolutionDocs.map((captchaSolutionDoc) => ({
153
+ updateOne: {
154
+ filter: { captchaId: captchaSolutionDoc.captchaId },
155
+ update: { $set: captchaSolutionDoc },
156
+ upsert: true,
157
+ },
158
+ })));
159
+ }
160
+ this.logger.debug("Dataset stored in database");
161
+ }
162
+ catch (err) {
163
+ throw new ProsopoDBError("DATABASE.DATASET_LOAD_FAILED", {
164
+ context: { failedFuncName: this.storeDataset.name, error: err },
165
+ logger: this.logger,
166
+ });
167
+ }
168
+ }
169
+ async getSolutions(datasetId) {
170
+ const docs = await this.tables?.solution
171
+ .find({ datasetId })
172
+ .lean();
173
+ return docs ? docs : [];
174
+ }
175
+ async getDataset(datasetId) {
176
+ const datasetDoc = await this.tables?.dataset
177
+ .findOne({ datasetId: datasetId })
178
+ .lean();
179
+ if (datasetDoc) {
180
+ const { datasetContentId, format, contentTree, solutionTree } = datasetDoc;
181
+ const captchas = (await this.tables?.captcha.find({ datasetId }).lean()) ||
182
+ [];
183
+ const solutions = (await this.tables?.solution
184
+ .find({ datasetId })
185
+ .lean()) || [];
186
+ const solutionsKeyed = {};
187
+ for (const solution of solutions) {
188
+ solutionsKeyed[solution.captchaId] = solution;
189
+ }
190
+ return {
191
+ datasetId,
192
+ datasetContentId,
193
+ format,
194
+ contentTree: contentTree || [],
195
+ solutionTree: solutionTree || [],
196
+ captchas: captchas.map((captchaDoc) => {
197
+ const { captchaId, captchaContentId, items, target, salt, solved } = captchaDoc;
198
+ const solution = solutionsKeyed[captchaId];
199
+ return {
200
+ captchaId,
201
+ captchaContentId,
202
+ solved: !!solved,
203
+ salt,
204
+ items,
205
+ target,
206
+ solution: solved && solution ? solution.solution : [],
207
+ };
208
+ }),
209
+ };
210
+ }
211
+ throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
212
+ context: { failedFuncName: this.getDataset.name, datasetId },
213
+ });
214
+ }
215
+ async getRandomCaptcha(solved, datasetId, size) {
216
+ if (!isHex(datasetId)) {
217
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
218
+ context: { failedFuncName: this.getRandomCaptcha.name, datasetId },
219
+ });
220
+ }
221
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
222
+ const cursor = this.tables?.captcha.aggregate([
223
+ { $match: { datasetId, solved } },
224
+ { $sample: { size: sampleSize } },
225
+ {
226
+ $project: {
227
+ datasetId: 1,
228
+ datasetContentId: 1,
229
+ captchaId: 1,
230
+ captchaContentId: 1,
231
+ items: 1,
232
+ target: 1,
233
+ },
234
+ },
235
+ ]);
236
+ const docs = await cursor;
237
+ if (docs?.length) {
238
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
239
+ }
240
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
241
+ context: {
242
+ failedFuncName: this.getRandomCaptcha.name,
243
+ solved,
244
+ datasetId,
245
+ size,
246
+ },
247
+ });
248
+ }
249
+ async getCaptchaById(captchaId) {
250
+ const cursor = this.tables?.captcha
251
+ .find({ captchaId: { $in: captchaId } })
252
+ .lean();
253
+ const docs = await cursor;
254
+ if (docs?.length) {
255
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
256
+ }
257
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
258
+ context: { failedFuncName: this.getCaptchaById.name, captchaId },
259
+ });
260
+ }
261
+ async updateCaptcha(captcha, datasetId) {
262
+ if (!isHex(datasetId)) {
263
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
264
+ context: { failedFuncName: this.updateCaptcha.name, datasetId },
265
+ });
266
+ }
267
+ try {
268
+ await this.tables?.captcha.updateOne({ datasetId }, { $set: captcha }, { upsert: false });
269
+ }
270
+ catch (err) {
271
+ throw new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
272
+ context: { failedFuncName: this.getDatasetDetails.name, error: err },
273
+ });
274
+ }
275
+ }
276
+ async removeCaptchas(captchaIds) {
277
+ await this.tables?.captcha.deleteMany({ captchaId: { $in: captchaIds } });
278
+ }
279
+ async getDatasetDetails(datasetId) {
280
+ if (!isHex(datasetId)) {
281
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
282
+ context: { failedFuncName: this.getDatasetDetails.name, datasetId },
283
+ });
284
+ }
285
+ const doc = await this.tables?.dataset
286
+ .findOne({ datasetId })
287
+ .lean();
288
+ if (doc) {
289
+ return doc;
290
+ }
291
+ throw new ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
292
+ context: {
293
+ failedFuncName: this.getDatasetDetails.name,
294
+ datasetId,
295
+ },
296
+ });
297
+ }
298
+ async storeDappUserSolution(captchas, commit) {
299
+ const commitmentRecord = UserCommitmentSchema.parse({
300
+ ...commit,
301
+ lastUpdatedTimestamp: Date.now(),
302
+ });
303
+ if (captchas.length) {
304
+ await this.tables?.commitment.updateOne({
305
+ id: commit.id,
306
+ }, commitmentRecord, { upsert: true });
307
+ const ops = captchas.map((captcha) => ({
308
+ updateOne: {
309
+ filter: { commitmentId: commit.id, captchaId: captcha.captchaId },
310
+ update: {
311
+ $set: {
312
+ captchaId: captcha.captchaId,
313
+ captchaContentId: captcha.captchaContentId,
314
+ salt: captcha.salt,
315
+ solution: captcha.solution,
316
+ commitmentId: commit.id,
317
+ processed: false,
318
+ },
319
+ },
320
+ upsert: true,
321
+ },
322
+ }));
323
+ await this.tables?.usersolution.bulkWrite(ops);
324
+ }
325
+ }
326
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, serverChecked = false, userSubmitted = false, storedStatus = StoredStatusNames.notStored, userSignature) {
327
+ const tables = this.getTables();
328
+ const powCaptchaRecord = {
329
+ challenge,
330
+ ...components,
331
+ ipAddress,
332
+ headers,
333
+ result: { status: CaptchaStatus.pending },
334
+ userSubmitted,
335
+ serverChecked,
336
+ difficulty,
337
+ providerSignature,
338
+ userSignature,
339
+ lastUpdatedTimestamp: Date.now(),
340
+ };
341
+ try {
342
+ await tables.powcaptcha.create(powCaptchaRecord);
343
+ this.logger.info("PowCaptcha record added successfully", {
344
+ challenge,
345
+ userSubmitted,
346
+ serverChecked,
347
+ storedStatus,
348
+ });
349
+ }
350
+ catch (error) {
351
+ this.logger.error("Failed to add PowCaptcha record", {
352
+ error,
353
+ challenge,
354
+ userSubmitted,
355
+ serverChecked,
356
+ storedStatus,
357
+ });
358
+ throw new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
359
+ context: {
360
+ error,
361
+ challenge,
362
+ userSubmitted,
363
+ serverChecked,
364
+ storedStatus,
365
+ },
366
+ logger: this.logger,
367
+ });
368
+ }
369
+ }
370
+ async getPowCaptchaRecordByChallenge(challenge) {
371
+ if (!this.tables) {
372
+ throw new ProsopoEnvError("DATABASE.DATABASE_UNDEFINED", {
373
+ context: { failedFuncName: this.getPowCaptchaRecordByChallenge.name },
374
+ logger: this.logger,
375
+ });
376
+ }
377
+ try {
378
+ const record = await this.tables.powcaptcha
379
+ .findOne({ challenge })
380
+ .lean();
381
+ if (record) {
382
+ this.logger.info("PowCaptcha record retrieved successfully", {
383
+ challenge,
384
+ });
385
+ return record;
386
+ }
387
+ this.logger.info("No PowCaptcha record found", { challenge });
388
+ return null;
389
+ }
390
+ catch (error) {
391
+ this.logger.error("Failed to retrieve PowCaptcha record", {
392
+ error,
393
+ challenge,
394
+ });
395
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
396
+ context: { error, challenge },
397
+ logger: this.logger,
398
+ });
399
+ }
400
+ }
401
+ async updatePowCaptchaRecord(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
402
+ const tables = this.getTables();
403
+ const timestamp = Date.now();
404
+ const update = {
405
+ result,
406
+ serverChecked,
407
+ userSubmitted,
408
+ userSignature,
409
+ lastUpdatedTimestamp: timestamp,
410
+ };
411
+ try {
412
+ const updateResult = await tables.powcaptcha.updateOne({ challenge }, {
413
+ $set: update,
414
+ });
415
+ if (updateResult.matchedCount === 0) {
416
+ this.logger.info("No PowCaptcha record found to update", {
417
+ challenge,
418
+ ...update,
419
+ });
420
+ throw new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
421
+ context: {
422
+ challenge,
423
+ ...update,
424
+ },
425
+ logger: this.logger,
426
+ });
427
+ }
428
+ this.logger.info("PowCaptcha record updated successfully", {
429
+ challenge,
430
+ ...update,
431
+ });
432
+ }
433
+ catch (error) {
434
+ this.logger.error("Failed to update PowCaptcha record", {
435
+ error,
436
+ challenge,
437
+ ...update,
438
+ });
439
+ throw new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
440
+ context: {
441
+ error,
442
+ challenge,
443
+ ...update,
444
+ },
445
+ logger: this.logger,
446
+ });
447
+ }
448
+ }
449
+ async getProcessedDappUserSolutions() {
450
+ const docs = await this.tables?.usersolution
451
+ .find({ processed: true })
452
+ .lean();
453
+ return docs || [];
454
+ }
455
+ async getProcessedDappUserCommitments() {
456
+ const docs = await this.tables?.commitment
457
+ .find({ processed: true })
458
+ .lean();
459
+ return docs || [];
460
+ }
461
+ async getCheckedDappUserCommitments() {
462
+ const docs = await this.tables?.commitment
463
+ .find({ [StoredStatusNames.serverChecked]: true })
464
+ .lean();
465
+ return docs || [];
466
+ }
467
+ async getUnstoredDappUserCommitments(limit = 1000, skip = 0) {
468
+ const docs = await this.tables?.commitment
469
+ .find({
470
+ $or: [
471
+ { storedStatus: { $ne: StoredStatusNames.stored } },
472
+ { storedStatus: { $exists: false } },
473
+ ],
474
+ })
475
+ .sort({ _id: 1 })
476
+ .skip(skip)
477
+ .limit(limit)
478
+ .lean();
479
+ return docs || [];
480
+ }
481
+ async markDappUserCommitmentsStored(commitmentIds) {
482
+ const updateDoc = {
483
+ storedAtTimestamp: Date.now(),
484
+ };
485
+ await this.tables?.commitment.updateMany({ id: { $in: commitmentIds } }, { $set: updateDoc }, { upsert: false });
486
+ }
487
+ async markDappUserCommitmentsChecked(commitmentIds) {
488
+ const updateDoc = {
489
+ [StoredStatusNames.serverChecked]: true,
490
+ lastUpdatedTimestamp: Date.now(),
491
+ };
492
+ await this.tables?.commitment.updateMany({ id: { $in: commitmentIds } }, { $set: updateDoc }, { upsert: false });
493
+ }
494
+ async getUnstoredDappUserPoWCommitments(limit = 1000, skip = 0) {
495
+ const docs = await this.tables?.powcaptcha
496
+ .find({
497
+ $or: [
498
+ { storedStatus: { $ne: StoredStatusNames.stored } },
499
+ { storedStatus: { $exists: false } },
500
+ ],
501
+ })
502
+ .sort({ _id: 1 })
503
+ .skip(skip)
504
+ .limit(limit)
505
+ .lean();
506
+ return docs || [];
507
+ }
508
+ async markDappUserPoWCommitmentsStored(challenges) {
509
+ const updateDoc = {
510
+ storedAtTimestamp: Date.now(),
511
+ };
512
+ await this.tables?.powcaptcha.updateMany({ challenge: { $in: challenges } }, { $set: updateDoc }, { upsert: false });
513
+ }
514
+ async markDappUserPoWCommitmentsChecked(challenges) {
515
+ const updateDoc = {
516
+ [StoredStatusNames.serverChecked]: true,
517
+ lastUpdatedTimestamp: Date.now(),
518
+ };
519
+ await this.tables?.powcaptcha.updateMany({ challenge: { $in: challenges } }, {
520
+ $set: updateDoc,
521
+ }, { upsert: false });
522
+ }
523
+ async storeSessionRecord(sessionRecord) {
524
+ try {
525
+ await this.tables.session.create(sessionRecord);
526
+ }
527
+ catch (err) {
528
+ throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
529
+ context: { error: err, sessionId: sessionRecord.sessionId },
530
+ logger: this.logger,
531
+ });
532
+ }
533
+ }
534
+ async checkAndRemoveSession(sessionId) {
535
+ try {
536
+ const session = await this.tables.session
537
+ .findOneAndDelete({ sessionId })
538
+ .lean();
539
+ return session || undefined;
540
+ }
541
+ catch (err) {
542
+ throw new ProsopoDBError("DATABASE.SESSION_CHECK_REMOVE_FAILED", {
543
+ context: { error: err, sessionId },
544
+ logger: this.logger,
545
+ });
546
+ }
547
+ }
548
+ async removeProcessedDappUserSolutions(commitmentIds) {
549
+ return this.tables?.usersolution.deleteMany({
550
+ processed: true,
551
+ commitmentId: { $in: commitmentIds },
552
+ });
553
+ }
554
+ async removeProcessedDappUserCommitments(commitmentIds) {
555
+ return this.tables?.commitment.deleteMany({
556
+ processed: true,
557
+ id: { $in: commitmentIds },
558
+ });
559
+ }
560
+ async storeDappUserPending(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress) {
561
+ if (!isHex(requestHash)) {
562
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
563
+ context: {
564
+ failedFuncName: this.storeDappUserPending.name,
565
+ requestHash,
566
+ },
567
+ });
568
+ }
569
+ const pendingRecord = {
570
+ accountId: userAccount,
571
+ pending: true,
572
+ salt,
573
+ requestHash,
574
+ deadlineTimestamp,
575
+ requestedAtTimestamp,
576
+ ipAddress,
577
+ };
578
+ await this.tables?.pending.updateOne({ requestHash: requestHash }, { $set: pendingRecord }, { upsert: true });
579
+ }
580
+ async getDappUserPending(requestHash) {
581
+ if (!isHex(requestHash)) {
582
+ throw new ProsopoEnvError("DATABASE.INVALID_HASH", {
583
+ context: { failedFuncName: this.getDappUserPending.name, requestHash },
584
+ });
585
+ }
586
+ const doc = await this.tables?.pending
587
+ .findOne({ requestHash: requestHash })
588
+ .lean();
589
+ if (doc) {
590
+ return doc;
591
+ }
592
+ throw new ProsopoEnvError("DATABASE.PENDING_RECORD_NOT_FOUND", {
593
+ context: { failedFuncName: this.getDappUserPending.name, requestHash },
594
+ });
595
+ }
596
+ async updateDappUserPendingStatus(requestHash) {
597
+ if (!isHex(requestHash)) {
598
+ throw new ProsopoEnvError("DATABASE.INVALID_HASH", {
599
+ context: {
600
+ failedFuncName: this.updateDappUserPendingStatus.name,
601
+ requestHash,
602
+ },
603
+ });
604
+ }
605
+ await this.tables?.pending.updateOne({ requestHash: requestHash }, {
606
+ $set: {
607
+ [CaptchaStatus.pending]: false,
608
+ },
609
+ }, { upsert: true });
610
+ }
611
+ async getAllCaptchasByDatasetId(datasetId, state) {
612
+ const cursor = this.tables?.captcha
613
+ .find({
614
+ datasetId,
615
+ solved: state === CaptchaStates.Solved,
616
+ })
617
+ .lean();
618
+ const docs = await cursor;
619
+ if (docs) {
620
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
621
+ }
622
+ throw new ProsopoEnvError("DATABASE.CAPTCHA_GET_FAILED");
623
+ }
624
+ async getAllDappUserSolutions(captchaId) {
625
+ const cursor = this.tables?.usersolution
626
+ ?.find({ captchaId: { $in: captchaId } })
627
+ .lean();
628
+ const docs = await cursor;
629
+ if (docs) {
630
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
631
+ }
632
+ throw new ProsopoEnvError("DATABASE.SOLUTION_GET_FAILED");
633
+ }
634
+ async getDatasetIdWithSolvedCaptchasOfSizeN(solvedCaptchaCount) {
635
+ const cursor = this.tables?.solution.aggregate([
636
+ {
637
+ $match: {},
638
+ },
639
+ {
640
+ $group: {
641
+ _id: "$datasetId",
642
+ count: { $sum: 1 },
643
+ },
644
+ },
645
+ {
646
+ $match: {
647
+ count: { $gte: solvedCaptchaCount },
648
+ },
649
+ },
650
+ {
651
+ $sample: { size: 1 },
652
+ },
653
+ ]);
654
+ const docs = await cursor;
655
+ if (docs?.length) {
656
+ return docs[0]._id;
657
+ }
658
+ throw new ProsopoDBError("DATABASE.DATASET_WITH_SOLUTIONS_GET_FAILED");
659
+ }
660
+ async getRandomSolvedCaptchasFromSingleDataset(datasetId, size) {
661
+ if (!isHex(datasetId)) {
662
+ throw new ProsopoDBError("DATABASE.INVALID_HASH", {
663
+ context: {
664
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
665
+ datasetId,
666
+ },
667
+ });
668
+ }
669
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
670
+ const cursor = this.tables?.solution.aggregate([
671
+ { $match: { datasetId } },
672
+ { $sample: { size: sampleSize } },
673
+ {
674
+ $project: {
675
+ captchaId: 1,
676
+ captchaContentId: 1,
677
+ solution: 1,
678
+ },
679
+ },
680
+ ]);
681
+ const docs = await cursor;
682
+ if (docs?.length) {
683
+ return docs;
684
+ }
685
+ throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
686
+ context: {
687
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
688
+ datasetId,
689
+ size,
690
+ },
691
+ });
692
+ }
693
+ async getDappUserSolutionById(commitmentId) {
694
+ const cursor = this.tables?.usersolution
695
+ ?.findOne({
696
+ commitmentId: commitmentId,
697
+ }, { projection: { _id: 0 } })
698
+ .lean();
699
+ const doc = await cursor;
700
+ if (doc) {
701
+ return doc;
702
+ }
703
+ throw new ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
704
+ context: { failedFuncName: this.getCaptchaById.name, commitmentId },
705
+ });
706
+ }
707
+ async getDappUserCommitmentById(commitmentId) {
708
+ const commitmentCursor = this.tables?.commitment
709
+ ?.findOne({ id: commitmentId })
710
+ .lean();
711
+ const doc = await commitmentCursor;
712
+ return doc ? doc : undefined;
713
+ }
714
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
715
+ const docs = await this.tables?.commitment
716
+ ?.find({ userAccount, dappAccount }, { _id: 0 }, { sort: { _id: -1 } })
717
+ .lean();
718
+ return docs ? docs : [];
719
+ }
720
+ async approveDappUserCommitment(commitmentId) {
721
+ try {
722
+ const result = { status: CaptchaStatus.approved };
723
+ const updateDoc = {
724
+ result,
725
+ lastUpdatedTimestamp: Date.now(),
726
+ };
727
+ await this.tables?.commitment
728
+ ?.findOneAndUpdate({ id: commitmentId }, { $set: updateDoc }, { upsert: false })
729
+ .lean();
730
+ }
731
+ catch (err) {
732
+ throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
733
+ context: { error: err, commitmentId },
734
+ });
735
+ }
736
+ }
737
+ async disapproveDappUserCommitment(commitmentId, reason) {
738
+ try {
739
+ const updateDoc = {
740
+ result: { status: CaptchaStatus.disapproved, reason },
741
+ lastUpdatedTimestamp: Date.now(),
742
+ };
743
+ await this.tables?.commitment
744
+ ?.findOneAndUpdate({ id: commitmentId }, { $set: updateDoc }, { upsert: false })
745
+ .lean();
746
+ }
747
+ catch (err) {
748
+ throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
749
+ context: { error: err, commitmentId },
750
+ });
751
+ }
752
+ }
753
+ async flagProcessedDappUserSolutions(captchaIds) {
754
+ try {
755
+ await this.tables?.usersolution
756
+ ?.updateMany({ captchaId: { $in: captchaIds } }, { $set: { processed: true } }, { upsert: false })
757
+ .lean();
758
+ }
759
+ catch (err) {
760
+ throw new ProsopoDBError("DATABASE.SOLUTION_FLAG_FAILED", {
761
+ context: { error: err, captchaIds },
762
+ });
763
+ }
764
+ }
765
+ async flagProcessedDappUserCommitments(commitmentIds) {
766
+ try {
767
+ const distinctCommitmentIds = [...new Set(commitmentIds)];
768
+ await this.tables?.commitment
769
+ ?.updateMany({ id: { $in: distinctCommitmentIds } }, { $set: { processed: true } }, { upsert: false })
770
+ .lean();
771
+ }
772
+ catch (err) {
773
+ throw new ProsopoDBError("DATABASE.COMMITMENT_FLAG_FAILED", {
774
+ context: { error: err, commitmentIds },
775
+ });
776
+ }
777
+ }
778
+ async getScheduledTaskStatus(taskId, status) {
779
+ const cursor = await this.tables?.scheduler
780
+ ?.findOne({ taskId: taskId, status: status })
781
+ .lean();
782
+ return cursor ? cursor : undefined;
783
+ }
784
+ async getLastScheduledTaskStatus(task, status) {
785
+ const lookup = { processName: task };
786
+ if (status) {
787
+ lookup.status = status;
788
+ }
789
+ const cursor = await this.tables?.scheduler
790
+ ?.findOne(lookup)
791
+ .sort({ datetime: -1 })
792
+ .limit(1)
793
+ .lean();
794
+ return cursor ? cursor : undefined;
795
+ }
796
+ async createScheduledTaskStatus(taskName, status) {
797
+ const now = new Date().getTime();
798
+ const doc = ScheduledTaskSchema.parse({
799
+ processName: taskName,
800
+ datetime: now,
801
+ status,
802
+ });
803
+ const taskRecord = await this.tables?.scheduler.create(doc);
804
+ return taskRecord._id;
805
+ }
806
+ async updateScheduledTaskStatus(taskId, status, result) {
807
+ const update = {
808
+ status,
809
+ updated: new Date().getTime(),
810
+ ...(result && { result }),
811
+ };
812
+ await this.tables?.scheduler.updateOne({ _id: taskId }, { $set: update }, {
813
+ upsert: false,
814
+ });
815
+ }
816
+ async updateClientRecords(clientRecords) {
817
+ const ops = clientRecords.map((record) => {
818
+ const clientRecord = {
819
+ account: record.account,
820
+ settings: record.settings,
821
+ };
822
+ return {
823
+ updateOne: {
824
+ filter: { account: record.account },
825
+ update: {
826
+ $set: clientRecord,
827
+ },
828
+ upsert: true,
829
+ },
830
+ };
831
+ });
832
+ await this.tables?.client.bulkWrite(ops);
833
+ }
834
+ async getClientRecord(account) {
835
+ const doc = await this.tables?.client
836
+ .findOne({ account })
837
+ .lean();
838
+ return doc ? doc : undefined;
839
+ }
840
+ async getIPBlockRuleRecord(ipAddress) {
841
+ const doc = await this.tables?.ipblockrules
842
+ .findOne({ ip: Number(ipAddress) })
843
+ .lean();
844
+ return doc ? doc : undefined;
845
+ }
846
+ async storeIPBlockRuleRecords(rules) {
847
+ await this.tables?.ipblockrules.bulkWrite(rules.map((rule) => ({
848
+ updateOne: {
849
+ filter: { ip: rule.ip },
850
+ update: { $set: rule },
851
+ upsert: true,
852
+ },
853
+ })));
854
+ }
855
+ async getUserBlockRuleRecord(userAccount, dappAccount) {
856
+ const doc = await this.tables?.userblockrules
857
+ .findOne({ dappAccount, userAccount })
858
+ .lean();
859
+ return doc ? doc : undefined;
860
+ }
861
+ async storeUserBlockRuleRecords(rules) {
862
+ await this.tables?.userblockrules.bulkWrite(rules.map((rule) => ({
863
+ updateOne: {
864
+ filter: {
865
+ dappAccount: rule.dappAccount,
866
+ userAccount: rule.userAccount,
867
+ },
868
+ update: { $set: rule },
869
+ upsert: true,
870
+ },
871
+ })));
872
+ }
873
+ }
874
+ //# sourceMappingURL=provider.js.map