From db0822b3947112bb587a05a32d847fc288aef803 Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Mon, 15 May 2023 15:43:08 +0200
Subject: [PATCH] [enh] Allow to call '/wot/requirements/:search' with a query
 param (e.g. '?pubkey=true') to optimize DB queries - close #1439

---
 app/lib/dal/fileDAL.ts                        | 136 ++++++++++--------
 app/lib/dal/indexDAL/abstract/IIndexDAO.ts    |   2 +
 app/lib/dal/indexDAL/leveldb/LevelDBIindex.ts |  10 ++
 app/lib/dal/indexDAL/sqlite/SqliteIIndex.ts   |  10 ++
 app/lib/dal/sqliteDAL/IdentityDAL.ts          |   4 +
 app/modules/bma/lib/controllers/wot.ts        |  10 +-
 app/service/IdentityService.ts                |   4 +
 test/integration/identity/identity-test.ts    |  36 +++++
 8 files changed, 153 insertions(+), 59 deletions(-)

diff --git a/app/lib/dal/fileDAL.ts b/app/lib/dal/fileDAL.ts
index 57c9eb988..0f1586155 100644
--- a/app/lib/dal/fileDAL.ts
+++ b/app/lib/dal/fileDAL.ts
@@ -13,12 +13,12 @@
 
 import * as fs from "fs";
 import * as path from "path";
-import { SQLiteDriver } from "./drivers/SQLiteDriver";
-import { ConfDAL } from "./fileDALs/ConfDAL";
-import { ConfDTO } from "../dto/ConfDTO";
-import { BlockDTO } from "../dto/BlockDTO";
-import { DBHead } from "../db/DBHead";
-import { DBIdentity, IdentityDAL } from "./sqliteDAL/IdentityDAL";
+import {SQLiteDriver} from "./drivers/SQLiteDriver";
+import {ConfDAL} from "./fileDALs/ConfDAL";
+import {ConfDTO} from "../dto/ConfDTO";
+import {BlockDTO} from "../dto/BlockDTO";
+import {DBHead} from "../db/DBHead";
+import {DBIdentity, IdentityDAL} from "./sqliteDAL/IdentityDAL";
 import {
   CindexEntry,
   FullCindexEntry,
@@ -31,56 +31,56 @@ import {
   SimpleUdEntryForWallet,
   SindexEntry,
 } from "../indexer";
-import { TransactionDTO } from "../dto/TransactionDTO";
-import { CertDAL, DBCert } from "./sqliteDAL/CertDAL";
-import { DBBlock } from "../db/DBBlock";
-import { DBMembership, MembershipDAL } from "./sqliteDAL/MembershipDAL";
-import { MerkleDTO } from "../dto/MerkleDTO";
-import { CommonConstants } from "../common-libs/constants";
-import { PowDAL } from "./fileDALs/PowDAL";
-import { Initiable } from "./sqliteDAL/Initiable";
-import { MetaDAL } from "./sqliteDAL/MetaDAL";
-import { DataErrors } from "../common-libs/errors";
-import { BasicRevocableIdentity, IdentityDTO } from "../dto/IdentityDTO";
-import { FileSystem } from "../system/directory";
-import { RustDbTx, RustServer, RustServerConf, Wot } from "../../../neon/lib";
-import { IIndexDAO } from "./indexDAL/abstract/IIndexDAO";
-import { BIndexDAO } from "./indexDAL/abstract/BIndexDAO";
-import { MIndexDAO } from "./indexDAL/abstract/MIndexDAO";
-import { SIndexDAO } from "./indexDAL/abstract/SIndexDAO";
-import { CIndexDAO } from "./indexDAL/abstract/CIndexDAO";
-import { IdentityForRequirements } from "../../service/BlockchainService";
-import { NewLogger } from "../logger";
-import { BlockchainDAO } from "./indexDAL/abstract/BlockchainDAO";
-import { WalletDAO } from "./indexDAL/abstract/WalletDAO";
-import { PeerDAO } from "./indexDAL/abstract/PeerDAO";
-import { DBTx } from "../db/DBTx";
-import { DBWallet } from "../db/DBWallet";
-import { Tristamp } from "../common/Tristamp";
-import { CFSCore } from "./fileDALs/CFSCore";
-import { Underscore } from "../common-libs/underscore";
-import { DBPeer } from "../db/DBPeer";
-import { MonitorFlushedIndex } from "../debug/MonitorFlushedIndex";
-import { cliprogram } from "../common-libs/programOptions";
-import { DividendDAO, UDSource } from "./indexDAL/abstract/DividendDAO";
-import { HttpSource, HttpUD } from "../../modules/bma/lib/dtos";
-import { GenericDAO } from "./indexDAL/abstract/GenericDAO";
-import { MonitorExecutionTime } from "../debug/MonitorExecutionTime";
-import { LevelDBDividend } from "./indexDAL/leveldb/LevelDBDividend";
-import { LevelDBBindex } from "./indexDAL/leveldb/LevelDBBindex";
-
-import { LevelUp } from "levelup";
-import { LevelDBBlockchain } from "./indexDAL/leveldb/LevelDBBlockchain";
-import { LevelDBSindex } from "./indexDAL/leveldb/LevelDBSindex";
-import { SqlitePeers } from "./indexDAL/sqlite/SqlitePeers";
-import { LevelDBWallet } from "./indexDAL/leveldb/LevelDBWallet";
-import { LevelDBCindex } from "./indexDAL/leveldb/LevelDBCindex";
-import { LevelDBIindex } from "./indexDAL/leveldb/LevelDBIindex";
-import { LevelDBMindex } from "./indexDAL/leveldb/LevelDBMindex";
-import { ConfDAO } from "./indexDAL/abstract/ConfDAO";
-import { ServerDAO } from "./server-dao";
-import { PeerDTO } from "../dto/PeerDTO";
-import { RustPendingTx } from "../../../neon/native/server";
+import {TransactionDTO} from "../dto/TransactionDTO";
+import {CertDAL, DBCert} from "./sqliteDAL/CertDAL";
+import {DBBlock} from "../db/DBBlock";
+import {DBMembership, MembershipDAL} from "./sqliteDAL/MembershipDAL";
+import {MerkleDTO} from "../dto/MerkleDTO";
+import {CommonConstants} from "../common-libs/constants";
+import {PowDAL} from "./fileDALs/PowDAL";
+import {Initiable} from "./sqliteDAL/Initiable";
+import {MetaDAL} from "./sqliteDAL/MetaDAL";
+import {DataErrors} from "../common-libs/errors";
+import {BasicRevocableIdentity, IdentityDTO} from "../dto/IdentityDTO";
+import {FileSystem} from "../system/directory";
+import {RustDbTx, RustServer, Wot} from "../../../neon/lib";
+import {IIndexDAO} from "./indexDAL/abstract/IIndexDAO";
+import {BIndexDAO} from "./indexDAL/abstract/BIndexDAO";
+import {MIndexDAO} from "./indexDAL/abstract/MIndexDAO";
+import {SIndexDAO} from "./indexDAL/abstract/SIndexDAO";
+import {CIndexDAO} from "./indexDAL/abstract/CIndexDAO";
+import {IdentityForRequirements} from "../../service/BlockchainService";
+import {NewLogger} from "../logger";
+import {BlockchainDAO} from "./indexDAL/abstract/BlockchainDAO";
+import {WalletDAO} from "./indexDAL/abstract/WalletDAO";
+import {PeerDAO} from "./indexDAL/abstract/PeerDAO";
+import {DBTx} from "../db/DBTx";
+import {DBWallet} from "../db/DBWallet";
+import {Tristamp} from "../common/Tristamp";
+import {CFSCore} from "./fileDALs/CFSCore";
+import {Underscore} from "../common-libs/underscore";
+import {DBPeer} from "../db/DBPeer";
+import {MonitorFlushedIndex} from "../debug/MonitorFlushedIndex";
+import {cliprogram} from "../common-libs/programOptions";
+import {DividendDAO, UDSource} from "./indexDAL/abstract/DividendDAO";
+import {HttpSource, HttpUD} from "../../modules/bma/lib/dtos";
+import {GenericDAO} from "./indexDAL/abstract/GenericDAO";
+import {MonitorExecutionTime} from "../debug/MonitorExecutionTime";
+import {LevelDBDividend} from "./indexDAL/leveldb/LevelDBDividend";
+import {LevelDBBindex} from "./indexDAL/leveldb/LevelDBBindex";
+
+import {LevelUp} from "levelup";
+import {LevelDBBlockchain} from "./indexDAL/leveldb/LevelDBBlockchain";
+import {LevelDBSindex} from "./indexDAL/leveldb/LevelDBSindex";
+import {SqlitePeers} from "./indexDAL/sqlite/SqlitePeers";
+import {LevelDBWallet} from "./indexDAL/leveldb/LevelDBWallet";
+import {LevelDBCindex} from "./indexDAL/leveldb/LevelDBCindex";
+import {LevelDBIindex} from "./indexDAL/leveldb/LevelDBIindex";
+import {LevelDBMindex} from "./indexDAL/leveldb/LevelDBMindex";
+import {ConfDAO} from "./indexDAL/abstract/ConfDAO";
+import {ServerDAO} from "./server-dao";
+import {PeerDTO} from "../dto/PeerDTO";
+import {RustPendingTx} from "../../../neon/native/server";
 
 const readline = require("readline");
 const indexer = require("../indexer").Indexer;
@@ -929,8 +929,30 @@ export class FileDAL implements ServerDAO {
         return i;
       })
     );
+    return this.fillIdentitiesRevocation(found);
+  }
+
+  async searchJustIdentitiesByPubkey(pubkey: string): Promise<DBIdentity[]> {
+    const pendings = await this.idtyDAL.getFromPubkey(pubkey);
+    const writtens = await this.iindexDAL.searchByPubkey(pubkey);
+    const nonPendings = Underscore.filter(writtens, (w: IindexEntry) => {
+      return Underscore.where(pendings, { pubkey: w.pub }).length == 0;
+    });
+    const found = pendings.concat(
+      nonPendings.map((i: any) => {
+        // Use the correct field
+        i.pubkey = i.pub;
+        return i;
+      })
+    );
+    return this.fillIdentitiesRevocation(found);
+  }
+
+  private async fillIdentitiesRevocation(
+    identities: DBIdentity[]
+  ): Promise<DBIdentity[]> {
     return await Promise.all<DBIdentity>(
-      found.map(async (f) => {
+      identities.map(async (f) => {
         const ms = await this.mindexDAL.getReducedMSForImplicitRevocation(
           f.pubkey
         );
diff --git a/app/lib/dal/indexDAL/abstract/IIndexDAO.ts b/app/lib/dal/indexDAL/abstract/IIndexDAO.ts
index 1c1a08cd8..ecf64d441 100644
--- a/app/lib/dal/indexDAL/abstract/IIndexDAO.ts
+++ b/app/lib/dal/indexDAL/abstract/IIndexDAO.ts
@@ -19,6 +19,8 @@ export interface IIndexDAO extends ReduceableDAO<IindexEntry> {
 
   searchThoseMatching(search: string): Promise<OldIindexEntry[]>;
 
+  searchByPubkey(search: string): Promise<OldIindexEntry[]>;
+
   getFullFromUID(uid: string): Promise<FullIindexEntry>;
 
   getFullFromPubkey(pub: string): Promise<FullIindexEntry>;
diff --git a/app/lib/dal/indexDAL/leveldb/LevelDBIindex.ts b/app/lib/dal/indexDAL/leveldb/LevelDBIindex.ts
index c53f3dfc4..e7157b34e 100644
--- a/app/lib/dal/indexDAL/leveldb/LevelDBIindex.ts
+++ b/app/lib/dal/indexDAL/leveldb/LevelDBIindex.ts
@@ -280,4 +280,14 @@ export class LevelDBIindex extends LevelDBTable<IindexEntry[]>
       .filter((u) => u.pub)
       .concat(pubIdentities.filter((p) => p.pub));
   }
+
+  async searchByPubkey(pub: string): Promise<OldIindexEntry[]> {
+    const identities = await this.findByPub(pub);
+    if (!identities.length) {
+      return [];
+    }
+    // TODO Why do we need to use reduce() on array ? This will merge items into one object
+    const mergedIdentities = OldTransformers.toOldIindexEntry(reduce(identities))
+    return [mergedIdentities];
+  }
 }
diff --git a/app/lib/dal/indexDAL/sqlite/SqliteIIndex.ts b/app/lib/dal/indexDAL/sqlite/SqliteIIndex.ts
index fdbadbdaf..2cf0aeb7f 100644
--- a/app/lib/dal/indexDAL/sqlite/SqliteIIndex.ts
+++ b/app/lib/dal/indexDAL/sqlite/SqliteIIndex.ts
@@ -263,4 +263,14 @@ export class SqliteIIndex extends SqliteTable<IindexEntry>
       ])
     ).map(OldTransformers.toOldIindexEntry);
   }
+
+  @MonitorExecutionTime()
+  async searchByPubkey(pub: string): Promise<OldIindexEntry[]> {
+    // TODO Why not need reduce() here ? As done in the LevelDB implementation
+    return (
+        await this.find("SELECT * FROM iindex WHERE pub = ?", [
+          pub
+        ])
+    ).map(OldTransformers.toOldIindexEntry);
+  }
 }
diff --git a/app/lib/dal/sqliteDAL/IdentityDAL.ts b/app/lib/dal/sqliteDAL/IdentityDAL.ts
index 96d602a0a..9716b13f7 100644
--- a/app/lib/dal/sqliteDAL/IdentityDAL.ts
+++ b/app/lib/dal/sqliteDAL/IdentityDAL.ts
@@ -328,6 +328,10 @@ export class IdentityDAL extends AbstractSQLite<DBIdentity> {
     });
   }
 
+  getFromPubkey(pubkey: string) {
+    return this.sqlFind({pubkey});
+  }
+
   async trimExpiredIdentities(medianTime: number) {
     await this.exec(
       "DELETE FROM " +
diff --git a/app/modules/bma/lib/controllers/wot.ts b/app/modules/bma/lib/controllers/wot.ts
index 17e57ed83..43c6e1e97 100644
--- a/app/modules/bma/lib/controllers/wot.ts
+++ b/app/modules/bma/lib/controllers/wot.ts
@@ -187,9 +187,15 @@ export class WOTBinding extends AbstractController {
 
   async requirements(req: any): Promise<HttpRequirements> {
     const search = await ParametersService.getSearchP(req);
-    const identities: any = await this.IdentityService.searchIdentities(search);
+    let identities: any = [];
+    if (req.query?.pubkey === true || req.query?.pubkey === "true") {
+      identities = await this.IdentityService.searchIdentitiesByPubkey(search);
+    }
+    else {
+      identities = await this.IdentityService.searchIdentities(search);
+    }
     const all: HttpIdentityRequirement[] = await this.BlockchainService.requirementsOfIdentities(
-      identities
+        identities
     );
     if (!all || !all.length) {
       throw BMAConstants.ERRORS.NO_IDTY_MATCHING_PUB_OR_UID;
diff --git a/app/service/IdentityService.ts b/app/service/IdentityService.ts
index 676b8602a..f3173f885 100644
--- a/app/service/IdentityService.ts
+++ b/app/service/IdentityService.ts
@@ -54,6 +54,10 @@ export class IdentityService extends FIFOService {
     return this.dal.searchJustIdentities(search);
   }
 
+  searchIdentitiesByPubkey(pubkey: string) {
+    return this.dal.searchJustIdentitiesByPubkey(pubkey);
+  }
+
   async findMember(search: string) {
     let idty = null;
     if (search.match(constants.PUBLIC_KEY)) {
diff --git a/test/integration/identity/identity-test.ts b/test/integration/identity/identity-test.ts
index 847ff19e8..d87331eff 100644
--- a/test/integration/identity/identity-test.ts
+++ b/test/integration/identity/identity-test.ts
@@ -291,6 +291,42 @@ describe("Identities collision", function() {
     });
   });
 
+  it('requirements by pubkey of cat', function() {
+    return expectAnswer(rp('http://127.0.0.1:7799/wot/requirements/HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd?pubkey=true', { json: true }), function(res:HttpRequirements) {
+      res.should.have.property('identities').be.an.Array;
+      res.should.have.property('identities').have.length(1);
+      res.identities[0].should.have.property('pubkey').equal('HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd');
+      res.identities[0].should.have.property('uid').equal('cat');
+      res.identities[0].should.have.property('meta').property('timestamp');
+      res.identities[0].should.have.property('wasMember').equal(true);
+      res.identities[0].should.have.property('expired').equal(false); // Because it has been a member once! So its identity will exist forever.
+      res.identities[0].should.have.property('outdistanced').equal(false);
+      res.identities[0].should.have.property('isSentry').equal(true); // dSen = 2, cat has issued and received 2 certs with tic and toc
+      res.identities[0].should.have.property('certifications').have.length(2);
+      res.identities[0].should.have.property('membershipPendingExpiresIn').equal(0);
+      res.identities[0].should.have.property('membershipExpiresIn').greaterThan(9000);
+    });
+  });
+
+  it('requirements by pubkey of man1', function() {
+    return expectAnswer(rp('http://127.0.0.1:7799/wot/requirements/12AbjvYY5hxV4v2KrN9pnGzgFxogwrzgYyncYHHsyFDK?pubkey=true', { json: true }), function(res:HttpRequirements) {
+      res.should.have.property('identities').be.an.Array;
+      res.should.have.property('identities').have.length(1);
+      res.identities[0].should.have.property('pubkey').equal('12AbjvYY5hxV4v2KrN9pnGzgFxogwrzgYyncYHHsyFDK');
+      res.identities[0].should.have.property('uid').equal('man1');
+      res.identities[0].should.have.property('meta').property('timestamp');
+      res.identities[0].should.have.property('expired').equal(false);
+      res.identities[0].should.have.property('outdistanced').equal(false);
+      res.identities[0].should.have.property('isSentry').equal(false); // Not a member, also dSen = 2, but man1 has only 1 certification
+      res.identities[0].should.have.property('certifications').length(1);
+      res.identities[0].certifications[0].should.have.property('from').equal('2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc');
+      res.identities[0].certifications[0].should.have.property('to').equal('12AbjvYY5hxV4v2KrN9pnGzgFxogwrzgYyncYHHsyFDK');
+      res.identities[0].certifications[0].should.have.property('expiresIn').greaterThan(0);
+      res.identities[0].should.have.property('membershipPendingExpiresIn').greaterThan(9000);
+      res.identities[0].should.have.property('membershipExpiresIn').equal(0);
+    });
+  });
+
   it('should have certified-by/tic giving results', function() {
     return expectAnswer(rp('http://127.0.0.1:7799/wot/certified-by/tic', { json: true }), function(res:HttpCertifications) {
       res.should.have.property('pubkey').equal('DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV');
-- 
GitLab