Skip to content

Commit 25e9ed3

Browse files
perf: Improve customer account query [DEV-4965] (#673)
* perf: Improve customer account query * perf: Cache customer accounts [DEV-4973] (#674) perf: Cache customer accounts * Regenerate lockfile * generate migration --------- Co-authored-by: Tasos Derisiotis <50984242+Eengineer1@users.noreply.github.com>
1 parent 1b44d52 commit 25e9ed3

11 files changed

Lines changed: 111 additions & 53 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/controllers/admin/organisation.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
} from '../../types/admin.js';
1313
import { PaymentAccountService } from '../../services/api/payment-account.js';
1414
import { CheqdNetwork } from '@cheqd/sdk';
15+
import { LocalStore } from '../../database/cache/store.js';
16+
import { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js';
1517

1618
dotenv.config();
1719

@@ -73,16 +75,23 @@ export class OrganisationController {
7375
email,
7476
description,
7577
});
76-
const paymentAccount = await PaymentAccountService.instance.find({ customer: customer });
7778

78-
if (!customer || paymentAccount.length === 0) {
79+
const cachedAccounts = LocalStore.instance.getCustomerAccounts(response.locals.customer.customerId);
80+
let paymentAccounts: PaymentAccountEntity[];
81+
if (cachedAccounts?.length == 2) {
82+
paymentAccounts = cachedAccounts;
83+
} else {
84+
paymentAccounts = await PaymentAccountService.instance.find({ customer: response.locals.customer });
85+
}
86+
87+
if (!customer || paymentAccounts.length === 0) {
7988
response.status(StatusCodes.NOT_FOUND).json({
8089
error: 'Customer for updating not found',
8190
} satisfies AdminOrganisationUpdateUnsuccessfulResponseBody);
8291
}
8392

84-
const testnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address;
85-
const mainnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address;
93+
const testnetAddress = paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address;
94+
const mainnetAddress = paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address;
8695

8796
return response.status(StatusCodes.OK).json({
8897
name: customer.name,

src/controllers/api/account.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { SubscriptionService } from '../../services/admin/subscription.js';
3232
import { RoleService } from '../../services/api/role.js';
3333
import { getStripeObjectKey } from '../../utils/index.js';
3434
import { KeyService } from '../../services/api/key.js';
35+
import { LocalStore } from '../../database/cache/store.js';
36+
3537
dotenv.config();
3638

3739
export class AccountController {
@@ -76,7 +78,14 @@ export class AccountController {
7678
error: 'Bad state cause there is no customer assigned to the user yet. Please contact administrator.',
7779
} satisfies UnsuccessfulQueryCustomerResponseBody);
7880
}
79-
const paymentAccounts = await PaymentAccountService.instance.find({ customer: response.locals.customer });
81+
82+
const cachedAccounts = LocalStore.instance.getCustomerAccounts(response.locals.customer.customerId);
83+
let paymentAccounts: PaymentAccountEntity[];
84+
if (cachedAccounts?.length == 2) {
85+
paymentAccounts = cachedAccounts;
86+
} else {
87+
paymentAccounts = await PaymentAccountService.instance.find({ customer: response.locals.customer });
88+
}
8089
const result: QueryCustomerResponseBody = {
8190
customer: {
8291
customerId: response.locals.customer.customerId,

src/database/cache/store.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import NodeCache from 'node-cache';
2+
import * as dotenv from 'dotenv';
3+
4+
import { PaymentAccountEntity } from '../entities/payment.account.entity.js';
5+
6+
dotenv.config();
7+
8+
let { LOCAL_STORE_TTL = 600 } = process.env;
9+
10+
export class LocalStore {
11+
private cache: NodeCache;
12+
13+
public static instance = new LocalStore();
14+
15+
constructor() {
16+
this.cache = new NodeCache();
17+
}
18+
19+
setCustomerAccounts(key: string, data: PaymentAccountEntity[]) {
20+
this.cache.set(key, data, +LOCAL_STORE_TTL);
21+
}
22+
23+
getCustomerAccounts(key: string) {
24+
return this.cache.get(key) as PaymentAccountEntity[] | undefined;
25+
}
26+
}

src/database/entities/payment.account.entity.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
1+
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
22

33
import * as dotenv from 'dotenv';
44
import { namespaceEnum } from './../types/enum.js';
@@ -8,6 +8,7 @@ import type { KeyEntity } from './key.entity.js';
88
dotenv.config();
99

1010
@Entity('paymentAccount')
11+
@Index(['customer', 'namespace'], { unique: true })
1112
export class PaymentAccountEntity {
1213
@Column({
1314
type: 'text',
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class IndexPaymentAccountTable1746513196390 implements MigrationInterface {
4+
public async up(queryRunner: QueryRunner): Promise<void> {
5+
await queryRunner.query(
6+
`CREATE UNIQUE INDEX "IDX_e252516d4b5a966d291ee0ab61" ON "paymentAccount" ("customerId", "namespace") `
7+
);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`DROP INDEX "public"."IDX_e252516d4b5a966d291ee0ab61"`);
12+
}
13+
}

src/database/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscr
4040
import { AlterAPIKeyTable1695740346004 } from '../migrations/AlterAPIKeyTable.js';
4141
import { AlterCustomerTableAddEmail1695740346005 } from '../migrations/AlterCustomerTableAddEmail.js';
4242
import { AlterCustomerTableUpdateEmail1695740346006 } from '../migrations/AlterCustomerTableUniqueEmail.js';
43+
import { IndexPaymentAccountTable1746513196390 } from '../migrations/IndexPaymentAccountTable.js';
44+
4345
dotenv.config();
4446

4547
const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env;
@@ -118,6 +120,8 @@ export class Postgres implements AbstractDatabase {
118120
AlterCustomerTableAddEmail1695740346005,
119121
// Add unique constraint to email field
120122
AlterCustomerTableUpdateEmail1695740346006,
123+
// Add unique index in PaymentAccount table
124+
IndexPaymentAccountTable1746513196390,
121125
],
122126
entities: [
123127
...Entities,

src/services/api/payment-account.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,17 @@ export class PaymentAccountService {
8484
});
8585
}
8686

87-
public async find(where: Record<string, unknown>) {
87+
public async find(where: Record<string, unknown>, relations?: string[]) {
8888
return await this.paymentAccountRepository.find({
8989
where: where,
90-
relations: ['customer', 'key'],
90+
relations,
91+
});
92+
}
93+
94+
public async findOne(where: Record<string, unknown>, relations?: string[]) {
95+
return await this.paymentAccountRepository.findOne({
96+
where: where,
97+
relations,
9198
});
9299
}
93100
}

src/services/api/store.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/services/identity/postgres.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import type { TPublicKeyEd25519 } from '@cheqd/did-provider-cheqd';
5757
import { toTPublicKeyEd25519 } from '../helpers.js';
5858
import type { APIServiceOptions } from '../../types/admin.js';
5959
import { SupportedKeyTypes } from '@veramo/utils';
60+
import { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js';
61+
import { LocalStore } from '../../database/cache/store.js';
6062

6163
dotenv.config();
6264

@@ -89,36 +91,32 @@ export class PostgresIdentityService extends DefaultIdentityService {
8991

9092
async createCheqdProvider(
9193
customer: CustomerEntity,
92-
namespace: CheqdNetwork
94+
namespace: CheqdNetwork,
95+
paymentAccounts: PaymentAccountEntity[]
9396
): Promise<CheqdDIDProvider | undefined> {
9497
let rpcUrl = '';
9598
if (namespace === CheqdNetwork.Mainnet) {
9699
rpcUrl = MAINNET_RPC_URL || DefaultRPCUrls.mainnet;
97100
} else {
98101
rpcUrl = TESTNET_RPC_URL || DefaultRPCUrls.testnet;
99102
}
100-
const paymentAccount = await PaymentAccountService.instance.find({
101-
namespace: namespace,
102-
customer: customer,
103-
});
104-
if (paymentAccount.length > 1) {
105-
throw new Error(`More than one payment account for ${namespace} found`);
103+
const paymentAccount = paymentAccounts.find((acc) => acc.namespace === namespace);
104+
if (paymentAccount === undefined) {
105+
return undefined;
106106
}
107-
if (paymentAccount.length === 1) {
108-
const privateKey = (await this.getPrivateKey(paymentAccount[0].key.kid))?.privateKeyHex;
109107

110-
if (!privateKey) {
111-
throw new Error(`No keys is initialized`);
112-
}
108+
const privateKey = (await this.getPrivateKey(paymentAccount.key.kid))?.privateKeyHex;
113109

114-
return new CheqdDIDProvider({
115-
defaultKms: 'postgres',
116-
cosmosPayerSeed: privateKey,
117-
networkType: namespace,
118-
rpcUrl: rpcUrl,
119-
});
110+
if (!privateKey) {
111+
throw new Error(`No keys is initialized`);
120112
}
121-
return undefined;
113+
114+
return new CheqdDIDProvider({
115+
defaultKms: 'postgres',
116+
cosmosPayerSeed: privateKey,
117+
networkType: namespace,
118+
rpcUrl: rpcUrl,
119+
});
122120
}
123121

124122
async createAgent(customer: CustomerEntity): Promise<VeramoAgent> {
@@ -133,10 +131,22 @@ export class PostgresIdentityService extends DefaultIdentityService {
133131
}
134132
const dbConnection = Connection.instance.dbConnection;
135133

134+
const cachedAccounts = LocalStore.instance.getCustomerAccounts(customer.customerId);
135+
let paymentAccounts: PaymentAccountEntity[];
136+
if (cachedAccounts?.length == 2) {
137+
paymentAccounts = cachedAccounts;
138+
} else {
139+
paymentAccounts = await PaymentAccountService.instance.find({ customer }, ['key']);
140+
141+
if (paymentAccounts.length > 0) {
142+
LocalStore.instance.setCustomerAccounts(customer.customerId, paymentAccounts);
143+
}
144+
}
145+
136146
// One customer may / may not have one Mainnet paymentAccount
137-
const providerMainnet = await this.createCheqdProvider(customer, CheqdNetwork.Mainnet);
147+
const providerMainnet = await this.createCheqdProvider(customer, CheqdNetwork.Mainnet, paymentAccounts);
138148
// One customer may / may not have one Testnet paymentAccount
139-
const providerTestnet = await this.createCheqdProvider(customer, CheqdNetwork.Testnet);
149+
const providerTestnet = await this.createCheqdProvider(customer, CheqdNetwork.Testnet, paymentAccounts);
140150
// did:key provider
141151
providers['did:key'] = new KeyDIDProvider({ defaultKms: 'postgres' });
142152
if (providerMainnet) {

0 commit comments

Comments
 (0)