@prosopo/database 2.1.5 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/base/index.d.ts +3 -0
  2. package/dist/base/index.d.ts.map +1 -0
  3. package/dist/base/index.js +3 -0
  4. package/dist/base/index.js.map +1 -0
  5. package/dist/base/mongo.d.ts +16 -0
  6. package/dist/base/mongo.d.ts.map +1 -0
  7. package/dist/base/mongo.js +89 -0
  8. package/dist/base/mongo.js.map +1 -0
  9. package/dist/base/mongoMemory.d.ts +11 -0
  10. package/dist/base/mongoMemory.d.ts.map +1 -0
  11. package/dist/base/mongoMemory.js +27 -0
  12. package/dist/base/mongoMemory.js.map +1 -0
  13. package/dist/captchaDatabase/captchaDatabse.d.ts +3 -0
  14. package/dist/captchaDatabase/captchaDatabse.d.ts.map +1 -0
  15. package/dist/captchaDatabase/captchaDatabse.js +22 -0
  16. package/dist/captchaDatabase/captchaDatabse.js.map +1 -0
  17. package/dist/captchaDatabase/index.d.ts +2 -0
  18. package/dist/captchaDatabase/index.d.ts.map +1 -0
  19. package/dist/captchaDatabase/index.js +2 -0
  20. package/dist/captchaDatabase/index.js.map +1 -0
  21. package/dist/cjs/base/index.cjs +6 -0
  22. package/dist/cjs/base/mongo.cjs +97 -0
  23. package/dist/cjs/base/mongoMemory.cjs +28 -0
  24. package/dist/cjs/databases/captcha.cjs +82 -0
  25. package/dist/cjs/databases/client.cjs +52 -0
  26. package/dist/cjs/databases/index.cjs +19 -0
  27. package/dist/cjs/databases/provider.cjs +983 -0
  28. package/dist/cjs/index.cjs +15 -0
  29. package/dist/databases/captcha.d.ts +16 -0
  30. package/dist/databases/captcha.d.ts.map +1 -0
  31. package/dist/databases/captcha.js +75 -0
  32. package/dist/databases/captcha.js.map +1 -0
  33. package/dist/databases/client.d.ts +12 -0
  34. package/dist/databases/client.d.ts.map +1 -0
  35. package/dist/databases/client.js +48 -0
  36. package/dist/databases/client.js.map +1 -0
  37. package/dist/databases/index.d.ts +16 -0
  38. package/dist/databases/index.d.ts.map +1 -0
  39. package/dist/databases/index.js +17 -0
  40. package/dist/databases/index.js.map +1 -0
  41. package/dist/databases/mongo.d.ts +58 -0
  42. package/dist/databases/mongo.d.ts.map +1 -0
  43. package/dist/databases/mongo.js +682 -0
  44. package/dist/databases/mongo.js.map +1 -0
  45. package/dist/databases/mongoMemory.d.ts +10 -0
  46. package/dist/databases/mongoMemory.d.ts.map +1 -0
  47. package/dist/databases/mongoMemory.js +30 -0
  48. package/dist/databases/mongoMemory.js.map +1 -0
  49. package/dist/databases/provider.d.ts +73 -0
  50. package/dist/databases/provider.d.ts.map +1 -0
  51. package/dist/databases/provider.js +823 -0
  52. package/dist/databases/provider.js.map +1 -0
  53. package/dist/eventsDatabase/eventsDatabase.d.ts +3 -0
  54. package/dist/eventsDatabase/eventsDatabase.d.ts.map +1 -0
  55. package/dist/eventsDatabase/eventsDatabase.js +46 -0
  56. package/dist/eventsDatabase/eventsDatabase.js.map +1 -0
  57. package/dist/eventsDatabase/index.d.ts +2 -0
  58. package/dist/eventsDatabase/index.d.ts.map +1 -0
  59. package/dist/eventsDatabase/index.js +2 -0
  60. package/dist/eventsDatabase/index.js.map +1 -0
  61. package/dist/index.d.ts +3 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js.map +1 -0
  64. package/package.json +5 -5
@@ -0,0 +1,983 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const is = require("@polkadot/util/is");
4
+ const common = require("@prosopo/common");
5
+ const types = require("@prosopo/types");
6
+ const typesDatabase = require("@prosopo/types-database");
7
+ const mongo = require("../base/mongo.cjs");
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["session"] = "session";
19
+ return TableNames2;
20
+ })(TableNames || {});
21
+ const PROVIDER_TABLES = [
22
+ {
23
+ collectionName: "captcha",
24
+ modelName: "Captcha",
25
+ schema: typesDatabase.CaptchaRecordSchema
26
+ },
27
+ {
28
+ collectionName: "powcaptcha",
29
+ modelName: "PowCaptcha",
30
+ schema: typesDatabase.PoWCaptchaRecordSchema
31
+ },
32
+ {
33
+ collectionName: "dataset",
34
+ modelName: "Dataset",
35
+ schema: typesDatabase.DatasetRecordSchema
36
+ },
37
+ {
38
+ collectionName: "solution",
39
+ modelName: "Solution",
40
+ schema: typesDatabase.SolutionRecordSchema
41
+ },
42
+ {
43
+ collectionName: "commitment",
44
+ modelName: "UserCommitment",
45
+ schema: typesDatabase.UserCommitmentRecordSchema
46
+ },
47
+ {
48
+ collectionName: "usersolution",
49
+ modelName: "UserSolution",
50
+ schema: typesDatabase.UserSolutionRecordSchema
51
+ },
52
+ {
53
+ collectionName: "pending",
54
+ modelName: "Pending",
55
+ schema: typesDatabase.PendingRecordSchema
56
+ },
57
+ {
58
+ collectionName: "scheduler",
59
+ modelName: "Scheduler",
60
+ schema: typesDatabase.ScheduledTaskRecordSchema
61
+ },
62
+ {
63
+ collectionName: "client",
64
+ modelName: "Client",
65
+ schema: typesDatabase.ClientRecordSchema
66
+ },
67
+ {
68
+ collectionName: "session",
69
+ modelName: "Session",
70
+ schema: typesDatabase.SessionRecordSchema
71
+ }
72
+ ];
73
+ class ProviderDatabase extends mongo.MongoDatabase {
74
+ constructor(url, dbname, authSource, logger) {
75
+ super(url, dbname, authSource, logger);
76
+ this.tables = {};
77
+ this.tables = {};
78
+ }
79
+ async connect() {
80
+ await super.connect();
81
+ this.loadTables();
82
+ }
83
+ loadTables() {
84
+ const tables = {};
85
+ PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => {
86
+ if (this.connection) {
87
+ tables[collectionName] = this.connection.model(modelName, schema);
88
+ }
89
+ });
90
+ this.tables = tables;
91
+ }
92
+ getTables() {
93
+ if (!this.tables) {
94
+ throw new common.ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
95
+ context: { failedFuncName: this.getTables.name },
96
+ logger: this.logger
97
+ });
98
+ }
99
+ return this.tables;
100
+ }
101
+ /**
102
+ * @description Load a dataset to the database
103
+ * @param {Dataset} dataset
104
+ */
105
+ async storeDataset(dataset) {
106
+ try {
107
+ this.logger.debug("Storing dataset in database");
108
+ const parsedDataset = types.DatasetWithIdsAndTreeSchema.parse(dataset);
109
+ const datasetDoc = {
110
+ datasetId: parsedDataset.datasetId,
111
+ datasetContentId: parsedDataset.datasetContentId,
112
+ format: parsedDataset.format,
113
+ contentTree: parsedDataset.contentTree,
114
+ solutionTree: parsedDataset.solutionTree
115
+ };
116
+ await this.tables.dataset?.updateOne(
117
+ { datasetId: parsedDataset.datasetId },
118
+ { $set: datasetDoc },
119
+ { upsert: true }
120
+ );
121
+ const captchaDocs = parsedDataset.captchas.map(
122
+ ({ solution, ...captcha }, index) => ({
123
+ ...captcha,
124
+ datasetId: parsedDataset.datasetId,
125
+ datasetContentId: parsedDataset.datasetContentId,
126
+ index,
127
+ solved: !!solution?.length
128
+ })
129
+ );
130
+ this.logger.debug("Inserting captcha records");
131
+ if (captchaDocs.length) {
132
+ await this.tables?.captcha.bulkWrite(
133
+ captchaDocs.map((captchaDoc) => ({
134
+ updateOne: {
135
+ filter: { captchaId: captchaDoc.captchaId },
136
+ update: { $set: captchaDoc },
137
+ upsert: true
138
+ }
139
+ }))
140
+ );
141
+ }
142
+ const captchaSolutionDocs = parsedDataset.captchas.filter(({ solution }) => solution?.length).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(
153
+ captchaSolutionDocs.map((captchaSolutionDoc) => ({
154
+ updateOne: {
155
+ filter: { captchaId: captchaSolutionDoc.captchaId },
156
+ update: { $set: captchaSolutionDoc },
157
+ upsert: true
158
+ }
159
+ }))
160
+ );
161
+ }
162
+ this.logger.debug("Dataset stored in database");
163
+ } catch (err) {
164
+ throw new common.ProsopoDBError("DATABASE.DATASET_LOAD_FAILED", {
165
+ context: { failedFuncName: this.storeDataset.name, error: err },
166
+ logger: this.logger
167
+ });
168
+ }
169
+ }
170
+ /** @description Get solutions for a dataset
171
+ * @param {string} datasetId
172
+ */
173
+ async getSolutions(datasetId) {
174
+ const docs = await this.tables?.solution.find({ datasetId }).lean();
175
+ return docs ? docs : [];
176
+ }
177
+ /** @description Get a dataset from the database
178
+ * @param {string} datasetId
179
+ */
180
+ async getDataset(datasetId) {
181
+ const datasetDoc = await this.tables?.dataset.findOne({ datasetId }).lean();
182
+ if (datasetDoc) {
183
+ const { datasetContentId, format, contentTree, solutionTree } = datasetDoc;
184
+ const captchas = await this.tables?.captcha.find({ datasetId }).lean() || [];
185
+ const solutions = await this.tables?.solution.find({ datasetId }).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 common.ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
212
+ context: { failedFuncName: this.getDataset.name, datasetId }
213
+ });
214
+ }
215
+ /**
216
+ * @description Get random captchas that are solved or not solved
217
+ * @param {boolean} solved `true` when captcha is solved
218
+ * @param {string} datasetId the id of the data set
219
+ * @param {number} size the number of records to be returned
220
+ */
221
+ async getRandomCaptcha(solved, datasetId, size) {
222
+ if (!is.isHex(datasetId)) {
223
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
224
+ context: { failedFuncName: this.getRandomCaptcha.name, datasetId }
225
+ });
226
+ }
227
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
228
+ const cursor = this.tables?.captcha.aggregate([
229
+ { $match: { datasetId, solved } },
230
+ { $sample: { size: sampleSize } },
231
+ {
232
+ $project: {
233
+ datasetId: 1,
234
+ datasetContentId: 1,
235
+ captchaId: 1,
236
+ captchaContentId: 1,
237
+ items: 1,
238
+ target: 1
239
+ }
240
+ }
241
+ ]);
242
+ const docs = await cursor;
243
+ if (docs?.length) {
244
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
245
+ }
246
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
247
+ context: {
248
+ failedFuncName: this.getRandomCaptcha.name,
249
+ solved,
250
+ datasetId,
251
+ size
252
+ }
253
+ });
254
+ }
255
+ /**
256
+ * @description Get captchas by id
257
+ * @param {string[]} captchaId
258
+ */
259
+ async getCaptchaById(captchaId) {
260
+ const cursor = this.tables?.captcha.find({ captchaId: { $in: captchaId } }).lean();
261
+ const docs = await cursor;
262
+ if (docs?.length) {
263
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
264
+ }
265
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
266
+ context: { failedFuncName: this.getCaptchaById.name, captchaId }
267
+ });
268
+ }
269
+ /**
270
+ * @description Update a captcha
271
+ * @param {Captcha} captcha
272
+ * @param {string} datasetId the id of the data set
273
+ */
274
+ async updateCaptcha(captcha, datasetId) {
275
+ if (!is.isHex(datasetId)) {
276
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
277
+ context: { failedFuncName: this.updateCaptcha.name, datasetId }
278
+ });
279
+ }
280
+ try {
281
+ await this.tables?.captcha.updateOne(
282
+ { datasetId },
283
+ { $set: captcha },
284
+ { upsert: false }
285
+ );
286
+ } catch (err) {
287
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
288
+ context: { failedFuncName: this.getDatasetDetails.name, error: err }
289
+ });
290
+ }
291
+ }
292
+ /**
293
+ * @description Remove captchas
294
+ */
295
+ async removeCaptchas(captchaIds) {
296
+ await this.tables?.captcha.deleteMany({ captchaId: { $in: captchaIds } });
297
+ }
298
+ /**
299
+ * @description Get a dataset by Id
300
+ */
301
+ async getDatasetDetails(datasetId) {
302
+ if (!is.isHex(datasetId)) {
303
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
304
+ context: { failedFuncName: this.getDatasetDetails.name, datasetId }
305
+ });
306
+ }
307
+ const doc = await this.tables?.dataset.findOne({ datasetId }).lean();
308
+ if (doc) {
309
+ return doc;
310
+ }
311
+ throw new common.ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
312
+ context: {
313
+ failedFuncName: this.getDatasetDetails.name,
314
+ datasetId
315
+ }
316
+ });
317
+ }
318
+ /**
319
+ * @description Store a Dapp User's captcha solution commitment
320
+ */
321
+ async storeDappUserSolution(captchas, commit) {
322
+ const commitmentRecord = typesDatabase.UserCommitmentSchema.parse({
323
+ ...commit,
324
+ lastUpdatedTimestamp: Date.now()
325
+ });
326
+ if (captchas.length) {
327
+ await this.tables?.commitment.updateOne(
328
+ {
329
+ id: commit.id
330
+ },
331
+ commitmentRecord,
332
+ { upsert: true }
333
+ );
334
+ const ops = captchas.map((captcha) => ({
335
+ updateOne: {
336
+ filter: { commitmentId: commit.id, captchaId: captcha.captchaId },
337
+ update: {
338
+ $set: {
339
+ captchaId: captcha.captchaId,
340
+ captchaContentId: captcha.captchaContentId,
341
+ salt: captcha.salt,
342
+ solution: captcha.solution,
343
+ commitmentId: commit.id,
344
+ processed: false
345
+ }
346
+ },
347
+ upsert: true
348
+ }
349
+ }));
350
+ await this.tables?.usersolution.bulkWrite(ops);
351
+ }
352
+ }
353
+ /**
354
+ * @description Adds a new PoW Captcha record to the database.
355
+ * @param {string} challenge The challenge string for the captcha.
356
+ * @param components The components of the PoW challenge.
357
+ * @param difficulty
358
+ * @param providerSignature
359
+ * @param ipAddress
360
+ * @param serverChecked
361
+ * @param userSubmitted
362
+ * @param storedStatus
363
+ * @param userSignature
364
+ * @returns {Promise<void>} A promise that resolves when the record is added.
365
+ */
366
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, serverChecked = false, userSubmitted = false, storedStatus = typesDatabase.StoredStatusNames.notStored, userSignature) {
367
+ const tables = this.getTables();
368
+ const powCaptchaRecord = {
369
+ challenge,
370
+ ...components,
371
+ ipAddress,
372
+ headers,
373
+ result: { status: types.CaptchaStatus.pending },
374
+ userSubmitted,
375
+ serverChecked,
376
+ difficulty,
377
+ providerSignature,
378
+ userSignature,
379
+ lastUpdatedTimestamp: Date.now()
380
+ };
381
+ try {
382
+ await tables.powcaptcha.create(powCaptchaRecord);
383
+ this.logger.info("PowCaptcha record added successfully", {
384
+ challenge,
385
+ userSubmitted,
386
+ serverChecked,
387
+ storedStatus
388
+ });
389
+ } catch (error) {
390
+ this.logger.error("Failed to add PowCaptcha record", {
391
+ error,
392
+ challenge,
393
+ userSubmitted,
394
+ serverChecked,
395
+ storedStatus
396
+ });
397
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
398
+ context: {
399
+ error,
400
+ challenge,
401
+ userSubmitted,
402
+ serverChecked,
403
+ storedStatus
404
+ },
405
+ logger: this.logger
406
+ });
407
+ }
408
+ }
409
+ /**
410
+ * @description Retrieves a PoW Captcha record by its challenge string.
411
+ * @param {string} challenge The challenge string to search for.
412
+ * @returns {Promise<PoWCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
413
+ */
414
+ async getPowCaptchaRecordByChallenge(challenge) {
415
+ if (!this.tables) {
416
+ throw new common.ProsopoEnvError("DATABASE.DATABASE_UNDEFINED", {
417
+ context: { failedFuncName: this.getPowCaptchaRecordByChallenge.name },
418
+ logger: this.logger
419
+ });
420
+ }
421
+ try {
422
+ const record = await this.tables.powcaptcha.findOne({ challenge }).lean();
423
+ if (record) {
424
+ this.logger.info("PowCaptcha record retrieved successfully", {
425
+ challenge
426
+ });
427
+ return record;
428
+ }
429
+ this.logger.info("No PowCaptcha record found", { challenge });
430
+ return null;
431
+ } catch (error) {
432
+ this.logger.error("Failed to retrieve PowCaptcha record", {
433
+ error,
434
+ challenge
435
+ });
436
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
437
+ context: { error, challenge },
438
+ logger: this.logger
439
+ });
440
+ }
441
+ }
442
+ /**
443
+ * @description Updates a PoW Captcha record in the database.
444
+ * @param {string} challenge The challenge string of the captcha to be updated.
445
+ * @param result
446
+ * @param serverChecked
447
+ * @param userSubmitted
448
+ * @param userSignature
449
+ * @returns {Promise<void>} A promise that resolves when the record is updated.
450
+ */
451
+ async updatePowCaptchaRecord(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
452
+ const tables = this.getTables();
453
+ const timestamp = Date.now();
454
+ const update = {
455
+ result,
456
+ serverChecked,
457
+ userSubmitted,
458
+ userSignature,
459
+ lastUpdatedTimestamp: timestamp
460
+ };
461
+ try {
462
+ const updateResult = await tables.powcaptcha.updateOne(
463
+ { challenge },
464
+ {
465
+ $set: update
466
+ }
467
+ );
468
+ if (updateResult.matchedCount === 0) {
469
+ this.logger.info("No PowCaptcha record found to update", {
470
+ challenge,
471
+ ...update
472
+ });
473
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
474
+ context: {
475
+ challenge,
476
+ ...update
477
+ },
478
+ logger: this.logger
479
+ });
480
+ }
481
+ this.logger.info("PowCaptcha record updated successfully", {
482
+ challenge,
483
+ ...update
484
+ });
485
+ } catch (error) {
486
+ this.logger.error("Failed to update PowCaptcha record", {
487
+ error,
488
+ challenge,
489
+ ...update
490
+ });
491
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
492
+ context: {
493
+ error,
494
+ challenge,
495
+ ...update
496
+ },
497
+ logger: this.logger
498
+ });
499
+ }
500
+ }
501
+ /** @description Get processed Dapp User captcha solutions from the user solution table
502
+ */
503
+ async getProcessedDappUserSolutions() {
504
+ const docs = await this.tables?.usersolution.find({ processed: true }).lean();
505
+ return docs || [];
506
+ }
507
+ /** @description Get processed Dapp User image captcha commitments from the commitments table
508
+ */
509
+ async getProcessedDappUserCommitments() {
510
+ const docs = await this.tables?.commitment.find({ processed: true }).lean();
511
+ return docs || [];
512
+ }
513
+ /** @description Get serverChecked Dapp User image captcha commitments from the commitments table
514
+ */
515
+ async getCheckedDappUserCommitments() {
516
+ const docs = await this.tables?.commitment.find({ [typesDatabase.StoredStatusNames.serverChecked]: true }).lean();
517
+ return docs || [];
518
+ }
519
+ /** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
520
+ * client's total
521
+ */
522
+ async getUnstoredDappUserCommitments() {
523
+ const docs = await this.tables?.commitment.find({
524
+ $or: [
525
+ { storedStatus: { $ne: typesDatabase.StoredStatusNames.stored } },
526
+ { storedStatus: { $exists: false } }
527
+ ]
528
+ }).lean();
529
+ return docs || [];
530
+ }
531
+ /** @description Mark a list of captcha commits as stored
532
+ */
533
+ async markDappUserCommitmentsStored(commitmentIds) {
534
+ const updateDoc = {
535
+ storedAtTimestamp: Date.now()
536
+ };
537
+ await this.tables?.commitment.updateMany(
538
+ { id: { $in: commitmentIds } },
539
+ { $set: updateDoc },
540
+ { upsert: false }
541
+ );
542
+ }
543
+ /** @description Mark a list of captcha commits as checked
544
+ */
545
+ async markDappUserCommitmentsChecked(commitmentIds) {
546
+ const updateDoc = {
547
+ [typesDatabase.StoredStatusNames.serverChecked]: true,
548
+ lastUpdatedTimestamp: Date.now()
549
+ };
550
+ await this.tables?.commitment.updateMany(
551
+ { id: { $in: commitmentIds } },
552
+ { $set: updateDoc },
553
+ { upsert: false }
554
+ );
555
+ }
556
+ /** @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
557
+ */
558
+ async getUnstoredDappUserPoWCommitments() {
559
+ const docs = await this.tables?.powcaptcha.find({
560
+ $or: [
561
+ { storedStatus: { $ne: typesDatabase.StoredStatusNames.stored } },
562
+ { storedStatus: { $exists: false } }
563
+ ]
564
+ }).lean();
565
+ return docs || [];
566
+ }
567
+ /** @description Mark a list of PoW captcha commits as stored
568
+ */
569
+ async markDappUserPoWCommitmentsStored(challenges) {
570
+ const updateDoc = {
571
+ storedAtTimestamp: Date.now()
572
+ };
573
+ await this.tables?.powcaptcha.updateMany(
574
+ { challenge: { $in: challenges } },
575
+ { $set: updateDoc },
576
+ { upsert: false }
577
+ );
578
+ }
579
+ /** @description Mark a list of PoW captcha commits as checked by the server
580
+ */
581
+ async markDappUserPoWCommitmentsChecked(challenges) {
582
+ const updateDoc = {
583
+ [typesDatabase.StoredStatusNames.serverChecked]: true,
584
+ lastUpdatedTimestamp: Date.now()
585
+ };
586
+ await this.tables?.powcaptcha.updateMany(
587
+ { challenge: { $in: challenges } },
588
+ {
589
+ $set: updateDoc
590
+ },
591
+ { upsert: false }
592
+ );
593
+ }
594
+ /**
595
+ * Store a new session record
596
+ */
597
+ async storeSessionRecord(sessionRecord) {
598
+ try {
599
+ await this.tables.session.create(sessionRecord);
600
+ } catch (err) {
601
+ throw new common.ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
602
+ context: { error: err, sessionId: sessionRecord.sessionId },
603
+ logger: this.logger
604
+ });
605
+ }
606
+ }
607
+ /**
608
+ * Check if a session exists and remove it if it does
609
+ * @returns The session record if it existed, undefined otherwise
610
+ */
611
+ async checkAndRemoveSession(sessionId) {
612
+ try {
613
+ const session = await this.tables.session.findOneAndDelete({ sessionId }).lean();
614
+ return session || void 0;
615
+ } catch (err) {
616
+ throw new common.ProsopoDBError("DATABASE.SESSION_CHECK_REMOVE_FAILED", {
617
+ context: { error: err, sessionId },
618
+ logger: this.logger
619
+ });
620
+ }
621
+ }
622
+ /** @description Remove processed Dapp User captcha solutions from the user solution table
623
+ */
624
+ async removeProcessedDappUserSolutions(commitmentIds) {
625
+ return this.tables?.usersolution.deleteMany({
626
+ processed: true,
627
+ commitmentId: { $in: commitmentIds }
628
+ });
629
+ }
630
+ /** @description Remove processed Dapp User captcha commitments from the user commitments table
631
+ */
632
+ async removeProcessedDappUserCommitments(commitmentIds) {
633
+ return this.tables?.commitment.deleteMany({
634
+ processed: true,
635
+ id: { $in: commitmentIds }
636
+ });
637
+ }
638
+ /**
639
+ * @description Store a Dapp User's pending record
640
+ */
641
+ async storeDappUserPending(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress) {
642
+ if (!is.isHex(requestHash)) {
643
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
644
+ context: {
645
+ failedFuncName: this.storeDappUserPending.name,
646
+ requestHash
647
+ }
648
+ });
649
+ }
650
+ const pendingRecord = {
651
+ accountId: userAccount,
652
+ pending: true,
653
+ salt,
654
+ requestHash,
655
+ deadlineTimestamp,
656
+ requestedAtTimestamp,
657
+ ipAddress
658
+ };
659
+ await this.tables?.pending.updateOne(
660
+ { requestHash },
661
+ { $set: pendingRecord },
662
+ { upsert: true }
663
+ );
664
+ }
665
+ /**
666
+ * @description Get a Dapp user's pending record
667
+ */
668
+ async getDappUserPending(requestHash) {
669
+ if (!is.isHex(requestHash)) {
670
+ throw new common.ProsopoEnvError("DATABASE.INVALID_HASH", {
671
+ context: { failedFuncName: this.getDappUserPending.name, requestHash }
672
+ });
673
+ }
674
+ const doc = await this.tables?.pending.findOne({ requestHash }).lean();
675
+ if (doc) {
676
+ return doc;
677
+ }
678
+ throw new common.ProsopoEnvError("DATABASE.PENDING_RECORD_NOT_FOUND", {
679
+ context: { failedFuncName: this.getDappUserPending.name, requestHash }
680
+ });
681
+ }
682
+ /**
683
+ * @description Mark a pending request as used
684
+ */
685
+ async updateDappUserPendingStatus(requestHash) {
686
+ if (!is.isHex(requestHash)) {
687
+ throw new common.ProsopoEnvError("DATABASE.INVALID_HASH", {
688
+ context: {
689
+ failedFuncName: this.updateDappUserPendingStatus.name,
690
+ requestHash
691
+ }
692
+ });
693
+ }
694
+ await this.tables?.pending.updateOne(
695
+ { requestHash },
696
+ {
697
+ $set: {
698
+ [types.CaptchaStatus.pending]: false
699
+ }
700
+ },
701
+ { upsert: true }
702
+ );
703
+ }
704
+ /**
705
+ * @description Get all unsolved captchas
706
+ */
707
+ async getAllCaptchasByDatasetId(datasetId, state) {
708
+ const cursor = this.tables?.captcha.find({
709
+ datasetId,
710
+ solved: state === types.CaptchaStates.Solved
711
+ }).lean();
712
+ const docs = await cursor;
713
+ if (docs) {
714
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
715
+ }
716
+ throw new common.ProsopoEnvError("DATABASE.CAPTCHA_GET_FAILED");
717
+ }
718
+ /**
719
+ * @description Get all dapp user solutions by captchaIds
720
+ */
721
+ async getAllDappUserSolutions(captchaId) {
722
+ const cursor = this.tables?.usersolution?.find({ captchaId: { $in: captchaId } }).lean();
723
+ const docs = await cursor;
724
+ if (docs) {
725
+ return docs.map(
726
+ ({ _id, ...keepAttrs }) => keepAttrs
727
+ );
728
+ }
729
+ throw new common.ProsopoEnvError("DATABASE.SOLUTION_GET_FAILED");
730
+ }
731
+ async getDatasetIdWithSolvedCaptchasOfSizeN(solvedCaptchaCount) {
732
+ const cursor = this.tables?.solution.aggregate([
733
+ {
734
+ $match: {}
735
+ },
736
+ {
737
+ $group: {
738
+ _id: "$datasetId",
739
+ count: { $sum: 1 }
740
+ }
741
+ },
742
+ {
743
+ $match: {
744
+ count: { $gte: solvedCaptchaCount }
745
+ }
746
+ },
747
+ {
748
+ $sample: { size: 1 }
749
+ }
750
+ ]);
751
+ const docs = await cursor;
752
+ if (docs?.length) {
753
+ return docs[0]._id;
754
+ }
755
+ throw new common.ProsopoDBError("DATABASE.DATASET_WITH_SOLUTIONS_GET_FAILED");
756
+ }
757
+ async getRandomSolvedCaptchasFromSingleDataset(datasetId, size) {
758
+ if (!is.isHex(datasetId)) {
759
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
760
+ context: {
761
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
762
+ datasetId
763
+ }
764
+ });
765
+ }
766
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
767
+ const cursor = this.tables?.solution.aggregate([
768
+ { $match: { datasetId } },
769
+ { $sample: { size: sampleSize } },
770
+ {
771
+ $project: {
772
+ captchaId: 1,
773
+ captchaContentId: 1,
774
+ solution: 1
775
+ }
776
+ }
777
+ ]);
778
+ const docs = await cursor;
779
+ if (docs?.length) {
780
+ return docs;
781
+ }
782
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
783
+ context: {
784
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
785
+ datasetId,
786
+ size
787
+ }
788
+ });
789
+ }
790
+ /**
791
+ * @description Get dapp user solution by ID
792
+ * @param {string[]} commitmentId
793
+ */
794
+ async getDappUserSolutionById(commitmentId) {
795
+ const cursor = this.tables?.usersolution?.findOne(
796
+ {
797
+ commitmentId
798
+ },
799
+ { projection: { _id: 0 } }
800
+ ).lean();
801
+ const doc = await cursor;
802
+ if (doc) {
803
+ return doc;
804
+ }
805
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
806
+ context: { failedFuncName: this.getCaptchaById.name, commitmentId }
807
+ });
808
+ }
809
+ /**
810
+ * @description Get dapp user commitment by user account
811
+ * @param commitmentId
812
+ */
813
+ async getDappUserCommitmentById(commitmentId) {
814
+ const commitmentCursor = this.tables?.commitment?.findOne({ id: commitmentId }).lean();
815
+ const doc = await commitmentCursor;
816
+ return doc ? doc : void 0;
817
+ }
818
+ /**
819
+ * @description Get dapp user commitment by user account
820
+ * @param {string} userAccount
821
+ * @param {string} dappAccount
822
+ */
823
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
824
+ const docs = await this.tables?.commitment?.find({ userAccount, dappAccount }, { _id: 0 }, { sort: { _id: -1 } }).lean();
825
+ return docs ? docs : [];
826
+ }
827
+ /**
828
+ * @description Approve a dapp user's solution
829
+ * @param {string[]} commitmentId
830
+ */
831
+ async approveDappUserCommitment(commitmentId) {
832
+ try {
833
+ const result = { status: types.CaptchaStatus.approved };
834
+ const updateDoc = {
835
+ result,
836
+ lastUpdatedTimestamp: Date.now()
837
+ };
838
+ await this.tables?.commitment?.findOneAndUpdate(
839
+ { id: commitmentId },
840
+ { $set: updateDoc },
841
+ { upsert: false }
842
+ ).lean();
843
+ } catch (err) {
844
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
845
+ context: { error: err, commitmentId }
846
+ });
847
+ }
848
+ }
849
+ /**
850
+ * @description Disapprove a dapp user's solution
851
+ * @param {string} commitmentId
852
+ * @param reason
853
+ */
854
+ async disapproveDappUserCommitment(commitmentId, reason) {
855
+ try {
856
+ const updateDoc = {
857
+ result: { status: types.CaptchaStatus.disapproved, reason },
858
+ lastUpdatedTimestamp: Date.now()
859
+ };
860
+ await this.tables?.commitment?.findOneAndUpdate(
861
+ { id: commitmentId },
862
+ { $set: updateDoc },
863
+ { upsert: false }
864
+ ).lean();
865
+ } catch (err) {
866
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
867
+ context: { error: err, commitmentId }
868
+ });
869
+ }
870
+ }
871
+ /**
872
+ * @description Flag a dapp user's solutions as used by calculated solution
873
+ * @param {string[]} captchaIds
874
+ */
875
+ async flagProcessedDappUserSolutions(captchaIds) {
876
+ try {
877
+ await this.tables?.usersolution?.updateMany(
878
+ { captchaId: { $in: captchaIds } },
879
+ { $set: { processed: true } },
880
+ { upsert: false }
881
+ ).lean();
882
+ } catch (err) {
883
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_FLAG_FAILED", {
884
+ context: { error: err, captchaIds }
885
+ });
886
+ }
887
+ }
888
+ /**
889
+ * @description Flag dapp users' commitments as used by calculated solution
890
+ * @param {string[]} commitmentIds
891
+ */
892
+ async flagProcessedDappUserCommitments(commitmentIds) {
893
+ try {
894
+ const distinctCommitmentIds = [...new Set(commitmentIds)];
895
+ await this.tables?.commitment?.updateMany(
896
+ { id: { $in: distinctCommitmentIds } },
897
+ { $set: { processed: true } },
898
+ { upsert: false }
899
+ ).lean();
900
+ } catch (err) {
901
+ throw new common.ProsopoDBError("DATABASE.COMMITMENT_FLAG_FAILED", {
902
+ context: { error: err, commitmentIds }
903
+ });
904
+ }
905
+ }
906
+ /**
907
+ * @description Get a scheduled task status record by task ID and status
908
+ */
909
+ async getScheduledTaskStatus(taskId, status) {
910
+ const cursor = await this.tables?.scheduler?.findOne({ taskId, status }).lean();
911
+ return cursor ? cursor : void 0;
912
+ }
913
+ /**
914
+ * @description Get the most recent scheduled task status record for a given task
915
+ */
916
+ async getLastScheduledTaskStatus(task, status) {
917
+ const lookup = { processName: task };
918
+ if (status) {
919
+ lookup.status = status;
920
+ }
921
+ const cursor = await this.tables?.scheduler?.findOne(lookup).sort({ datetime: -1 }).limit(1).lean();
922
+ return cursor ? cursor : void 0;
923
+ }
924
+ /**
925
+ * @description Create the status of a scheduled task
926
+ */
927
+ async createScheduledTaskStatus(taskName, status) {
928
+ const now = (/* @__PURE__ */ new Date()).getTime();
929
+ const doc = typesDatabase.ScheduledTaskSchema.parse({
930
+ processName: taskName,
931
+ datetime: now,
932
+ status
933
+ });
934
+ const taskRecord = await this.tables?.scheduler.create(doc);
935
+ return taskRecord._id;
936
+ }
937
+ /**
938
+ * @description Update the status of a scheduled task and an optional result
939
+ */
940
+ async updateScheduledTaskStatus(taskId, status, result) {
941
+ const update = {
942
+ status,
943
+ updated: (/* @__PURE__ */ new Date()).getTime(),
944
+ ...result && { result }
945
+ };
946
+ await this.tables?.scheduler.updateOne(
947
+ { _id: taskId },
948
+ { $set: update },
949
+ {
950
+ upsert: false
951
+ }
952
+ );
953
+ }
954
+ /**
955
+ * @description Update the client records
956
+ */
957
+ async updateClientRecords(clientRecords) {
958
+ const ops = clientRecords.map((record) => {
959
+ const clientRecord = {
960
+ account: record.account,
961
+ settings: record.settings
962
+ };
963
+ return {
964
+ updateOne: {
965
+ filter: { account: record.account },
966
+ update: {
967
+ $set: clientRecord
968
+ },
969
+ upsert: true
970
+ }
971
+ };
972
+ });
973
+ await this.tables?.client.bulkWrite(ops);
974
+ }
975
+ /**
976
+ * @description Get a client record
977
+ */
978
+ async getClientRecord(account) {
979
+ const doc = await this.tables?.client.findOne({ account }).lean();
980
+ return doc ? doc : void 0;
981
+ }
982
+ }
983
+ exports.ProviderDatabase = ProviderDatabase;