From d6f8189a9677397916c852aff15624824988b438 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Sep 2025 14:07:19 +0530 Subject: [PATCH 1/6] feat: added cache versioning to sql for geo invalidation --- backend/e2e-test/mocks/keystore.ts | 9 ++ backend/e2e-test/vitest-environment-knex.ts | 4 +- backend/src/@types/knex.d.ts | 8 ++ .../20250908193226_sql-cache_int.ts | 17 ++++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/key-value-store.ts | 18 ++++ backend/src/db/schemas/models.ts | 3 +- .../secret-approval-request-service.ts | 2 +- .../secret-replication-service.ts | 2 +- .../secret-rotation-queue.ts | 3 +- backend/src/keystore/key-value-store-dal.ts | 91 +++++++++++++++++++ backend/src/keystore/keystore.ts | 73 +++++++++------ backend/src/keystore/memory.ts | 9 ++ backend/src/lib/config/env.ts | 1 + backend/src/lib/knex/index.ts | 8 +- backend/src/main.ts | 4 +- backend/src/server/routes/index.ts | 7 +- .../folder-commit/folder-commit-service.ts | 2 +- .../resource-cleanup-queue.ts | 10 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 7 +- .../secret-v2-bridge-service.ts | 41 +++++---- 21 files changed, 258 insertions(+), 62 deletions(-) create mode 100644 backend/src/db/migrations/20250908193226_sql-cache_int.ts create mode 100644 backend/src/db/schemas/key-value-store.ts create mode 100644 backend/src/keystore/key-value-store-dal.ts diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 91b64ff0d..0cebe6ef3 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -56,6 +56,15 @@ export const mockKeyStore = (): TKeyStoreFactory => { incrementBy: async () => { return 1; }, + pgGetIntItem: async (key) => { + const value = store[key]; + if (typeof value === "number") { + return Number(value); + } + }, + pgIncrementBy: async () => { + return 1; + }, getItems: async (keys) => { const values = keys.map((key) => { const value = store[key]; diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index ff5f42286..085b8fe30 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -15,6 +15,7 @@ import { mockSmtpServer } from "./mocks/smtp"; import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { buildRedisFromConfig } from "@app/lib/config/redis"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; @@ -62,7 +63,8 @@ export default { const smtp = mockSmtpServer(); const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(envCfg); + const keyValueStoreDAL = keyValueStoreDALFactory(db); + const keyStore = keyStoreFactory(envCfg, keyValueStoreDAL); await queue.initialize(); diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 525ad1619..d14084885 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -191,6 +191,9 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKeyValueStore, + TKeyValueStoreInsert, + TKeyValueStoreUpdate, TKmipClientCertificates, TKmipClientCertificatesInsert, TKmipClientCertificatesUpdate, @@ -1264,5 +1267,10 @@ declare module "knex/types/tables" { TUserNotificationsInsert, TUserNotificationsUpdate >; + [TableName.KeyValueStore]: KnexOriginal.CompositeTableType< + TKeyValueStore, + TKeyValueStoreInsert, + TKeyValueStoreUpdate + >; } } diff --git a/backend/src/db/migrations/20250908193226_sql-cache_int.ts b/backend/src/db/migrations/20250908193226_sql-cache_int.ts new file mode 100644 index 000000000..dc4060d02 --- /dev/null +++ b/backend/src/db/migrations/20250908193226_sql-cache_int.ts @@ -0,0 +1,17 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.KeyValueStore))) { + await knex.schema.createTable(TableName.KeyValueStore, (t) => { + t.text("key").primary(); + t.bigint("integerValue"); + t.datetime("expiresAt"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.KeyValueStore); +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 1642c3555..e63ac4901 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -61,6 +61,7 @@ export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-certificate-authorities"; export * from "./internal-kms"; +export * from "./key-value-store"; export * from "./kmip-client-certificates"; export * from "./kmip-clients"; export * from "./kmip-org-configs"; diff --git a/backend/src/db/schemas/key-value-store.ts b/backend/src/db/schemas/key-value-store.ts new file mode 100644 index 000000000..d6ba90b2f --- /dev/null +++ b/backend/src/db/schemas/key-value-store.ts @@ -0,0 +1,18 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KeyValueStoreSchema = z.object({ + key: z.string(), + integerValue: z.coerce.number().nullable().optional(), + expiresAt: z.date().nullable().optional() +}); + +export type TKeyValueStore = z.infer; +export type TKeyValueStoreInsert = Omit, TImmutableDBKeys>; +export type TKeyValueStoreUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3e3e81fd0..37abc2b61 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -179,7 +179,8 @@ export enum TableName { SecretScanningConfig = "secret_scanning_configs", // reminders Reminder = "reminders", - ReminderRecipient = "reminders_recipients" + ReminderRecipient = "reminders_recipients", + KeyValueStore = "key_value_store" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index d485f7ea0..aa3e3dc0f 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -976,6 +976,7 @@ export const secretApprovalRequestServiceFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret }, approval: updatedSecretApproval @@ -983,7 +984,6 @@ export const secretApprovalRequestServiceFactory = ({ }); } - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); if (!folder) { diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index db41d00f7..93147d9e4 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -509,9 +509,9 @@ export const secretReplicationServiceFactory = ({ tx ); } + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); }); - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await secretQueueService.syncSecrets({ projectId, orgId, diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 1d5c1cedf..557e71e6c 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -361,9 +361,8 @@ export const secretRotationQueueFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId, tx); }); - - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId); } else { if (!botKey) throw new NotFoundError({ diff --git a/backend/src/keystore/key-value-store-dal.ts b/backend/src/keystore/key-value-store-dal.ts new file mode 100644 index 000000000..bdf5e8725 --- /dev/null +++ b/backend/src/keystore/key-value-store-dal.ts @@ -0,0 +1,91 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, TOrmify } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; +import { QueueName } from "@app/queue"; + +export interface TKeyValueStoreDALFactory extends TOrmify { + incrementBy: (key: string, dto: { incr?: number; tx?: Knex; expiresAt?: Date }) => Promise; + findOneInt: (key: string, tx?: Knex) => Promise; + pruneExpiredKeys: () => Promise; +} + +const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes +const CACHE_KEY_PRUNE_BATCH_SIZE = 10000; +const MAX_RETRY_ON_FAILURE = 3; + +export const keyValueStoreDALFactory = (db: TDbClient): TKeyValueStoreDALFactory => { + const keyValueStoreOrm = ormify(db, TableName.KeyValueStore); + + const incrementBy: TKeyValueStoreDALFactory["incrementBy"] = (key, { incr = 1, tx, expiresAt }) => { + return (tx || db)(TableName.KeyValueStore) + .insert({ key, integerValue: 1, expiresAt }) + .onConflict("key") + .merge({ + integerValue: db.raw(`"${TableName.KeyValueStore}"."integerValue" + ?`, [incr]), + expiresAt + }) + .returning("integerValue"); + }; + + const findOneInt: TKeyValueStoreDALFactory["findOneInt"] = async (key, tx) => { + const doc = await (tx || db.replicaNode())(TableName.KeyValueStore) + .where({ key }) + .andWhere( + (builder) => + void builder + .whereNull("expiresAt") // no expiry + .orWhere("expiresAt", ">", db.fn.now()) // or not expired + ) + .first() + .select("integerValue"); + return Number(doc?.integerValue || 0); + }; + + // delete all audit log that have expired + const pruneExpiredKeys: TKeyValueStoreDALFactory["pruneExpiredKeys"] = async () => { + let deletedIds: { key: string }[] = []; + let numberOfRetryOnFailure = 0; + let isRetrying = false; + + logger.info(`${QueueName.DailyResourceCleanUp}: db key value store clean up started`); + do { + try { + // eslint-disable-next-line no-await-in-loop + deletedIds = await db.transaction(async (trx) => { + await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`); + + const findExpiredKeysSubQuery = trx(TableName.KeyValueStore) + .where("expiresAt", "<", db.fn.now()) + .select("key") + .limit(CACHE_KEY_PRUNE_BATCH_SIZE); + + // eslint-disable-next-line no-await-in-loop + const results = await trx(TableName.KeyValueStore) + .whereIn("key", findExpiredKeysSubQuery) + .del() + .returning("key"); + + return results; + }); + + numberOfRetryOnFailure = 0; // reset + } catch (error) { + numberOfRetryOnFailure += 1; + deletedIds = []; + logger.error(error, "Failed to clean up db key value"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); + } + isRetrying = numberOfRetryOnFailure > 0; + } while (deletedIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE)); + logger.info(`${QueueName.DailyResourceCleanUp}: db key value store clean up completed`); + }; + + return { ...keyValueStoreOrm, incrementBy, findOneInt, pruneExpiredKeys }; +}; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index a6135e0ca..db6adebf1 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,11 +1,15 @@ import { Cluster, Redis } from "ioredis"; +import { Knex } from "knex"; import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; +import { ms } from "@app/lib/ms"; import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock"; +import { TKeyValueStoreDALFactory } from "./key-value-store-dal"; + export const PgSqlLock = { BootUpMigration: 2023, SuperAdminInit: 2024, @@ -95,13 +99,17 @@ export type TKeyStoreFactory = { deleteItemsByKeyIn: (keys: string[]) => Promise; deleteItems: (arg: TDeleteItems) => Promise; incrementBy: (key: string, value: number) => Promise; + getKeysByPattern: (pattern: string, limit?: number) => Promise; + // pg + pgIncrementBy: (key: string, dto: { incr?: number; expiry?: string; tx?: Knex }) => Promise; + pgGetIntItem: (key: string, prefix?: string) => Promise; + // locks acquireLock( resources: string[], duration: number, settings?: Partial ): Promise<{ release: () => Promise }>; waitTillReady: ({ key, waitingCb, keyCheckCb, waitIteration, delay, jitter }: TWaitTillReady) => Promise; - getKeysByPattern: (pattern: string, limit?: number) => Promise; }; const pickPrimaryOrSecondaryRedis = (primary: Redis | Cluster, secondaries?: Array) => { @@ -114,7 +122,10 @@ interface TKeyStoreFactoryDTO extends TRedisConfigKeys { REDIS_READ_REPLICAS?: { host: string; port: number }[]; } -export const keyStoreFactory = (redisConfigKeys: TKeyStoreFactoryDTO): TKeyStoreFactory => { +export const keyStoreFactory = ( + redisConfigKeys: TKeyStoreFactoryDTO, + keyValueStoreDAL: TKeyValueStoreDALFactory +): TKeyStoreFactory => { const primaryRedis = buildRedisFromConfig(redisConfigKeys); const redisReadReplicas = redisConfigKeys.REDIS_READ_REPLICAS?.map((el) => { if (redisConfigKeys.REDIS_URL) { @@ -189,29 +200,6 @@ export const keyStoreFactory = (redisConfigKeys: TKeyStoreFactoryDTO): TKeyStore const setExpiry = async (key: string, expiryInSeconds: number) => primaryRedis.expire(key, expiryInSeconds); - const waitTillReady = async ({ - key, - waitingCb, - keyCheckCb, - waitIteration = 10, - delay = 1000, - jitter = 200 - }: TWaitTillReady) => { - let attempts = 0; - let isReady = keyCheckCb(await getItem(key)); - while (!isReady) { - if (attempts > waitIteration) return; - // eslint-disable-next-line - await new Promise((resolve) => { - waitingCb?.(); - setTimeout(resolve, Math.max(0, applyJitter(delay, jitter))); - }); - attempts += 1; - // eslint-disable-next-line - isReady = keyCheckCb(await getItem(key)); - } - }; - const getKeysByPattern = async (pattern: string, limit?: number) => { let cursor = "0"; const allKeys: string[] = []; @@ -236,6 +224,37 @@ export const keyStoreFactory = (redisConfigKeys: TKeyStoreFactoryDTO): TKeyStore return allKeys; }; + const pgIncrementBy: TKeyStoreFactory["pgIncrementBy"] = async (key, { incr = 1, tx, expiry }) => { + const expiresAt = expiry ? new Date(Date.now() + ms(expiry)) : undefined; + return keyValueStoreDAL.incrementBy(key, { incr, expiresAt, tx }); + }; + + const pgGetIntItem = async (key: string, prefix?: string) => + keyValueStoreDAL.findOneInt(prefix ? `${prefix}:${key}` : key); + + const waitTillReady = async ({ + key, + waitingCb, + keyCheckCb, + waitIteration = 10, + delay = 1000, + jitter = 200 + }: TWaitTillReady) => { + let attempts = 0; + let isReady = keyCheckCb(await getItem(key)); + while (!isReady) { + if (attempts > waitIteration) return; + // eslint-disable-next-line + await new Promise((resolve) => { + waitingCb?.(); + setTimeout(resolve, Math.max(0, applyJitter(delay, jitter))); + }); + attempts += 1; + // eslint-disable-next-line + isReady = keyCheckCb(await getItem(key)); + } + }; + return { setItem, getItem, @@ -250,6 +269,8 @@ export const keyStoreFactory = (redisConfigKeys: TKeyStoreFactoryDTO): TKeyStore waitTillReady, getKeysByPattern, deleteItemsByKeyIn, - getItems + getItems, + pgGetIntItem, + pgIncrementBy }; }; diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index cf9ba83bd..2f9b77ced 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -53,6 +53,15 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { } return null; }, + pgGetIntItem: async (key) => { + const value = store[key]; + if (typeof value === "number") { + return Number(value); + } + }, + pgIncrementBy: async () => { + return 1; + }, incrementBy: async () => { return 1; }, diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index c4170aac3..ca76afbd1 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -410,6 +410,7 @@ const envSchema = z Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID) && Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET) && Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET), + isSecondaryInstance: Boolean(data.INFISICAL_PRIMARY_INSTANCE_URL), isHsmConfigured: Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 090df561a..dbfd29b81 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -250,12 +250,12 @@ export const ormify = ( .returning("*"); if ($incr) { Object.entries($incr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.increment(incrementField, incrementValue as number); }); } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.decrement(incrementField, incrementValue); + void query.decrement(incrementField, incrementValue as number); }); } const [docs] = await query; @@ -273,12 +273,12 @@ export const ormify = ( // increment and decrement operation in update if ($incr) { Object.entries($incr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.increment(incrementField, incrementValue as number); }); } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue); + void query.increment(incrementField, incrementValue as number); }); } return (await query) as Tables[Tname]["base"][]; diff --git a/backend/src/main.ts b/backend/src/main.ts index 8af47eb0b..7be9f43ec 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,7 @@ import "./lib/telemetry/instrumentation"; import dotenv from "dotenv"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; @@ -54,7 +55,8 @@ const run = async () => { await queue.initialize(); - const keyStore = keyStoreFactory(envConfig); + const keyValueStoreDAL = keyValueStoreDALFactory(db); + const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL); const redis = buildRedisFromConfig(envConfig); const hsmModule = initializeHsmModule(envConfig); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4803a6cd2..f79362b69 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -123,6 +123,7 @@ import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-grou import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; +import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, TEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -507,6 +508,7 @@ export const registerRoutes = async ( const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db); const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db); const secretScanningV2DAL = secretScanningV2DALFactory(db); + const keyValueStoreDAL = keyValueStoreDALFactory(db); const eventBusService = eventBusFactory(server.redis); const sseService = sseServiceFactory(eventBusService, server.redis); @@ -643,6 +645,7 @@ export const registerRoutes = async ( const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db); const folderCommitDAL = folderCommitDALFactory(db); const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db); + const folderCommitQueueService = folderCommitQueueServiceFactory({ queueService, folderTreeCheckpointDAL, @@ -1682,6 +1685,7 @@ export const registerRoutes = async ( userDAL, identityDAL }); + const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, @@ -1694,7 +1698,8 @@ export const registerRoutes = async ( identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL, serviceTokenService, orgService, - userNotificationDAL + userNotificationDAL, + keyValueStoreDAL }); const dailyReminderQueueService = dailyReminderQueueServiceFactory({ diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 470edbbba..e4c151ff1 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -1386,7 +1386,7 @@ export const folderCommitServiceFactory = ({ ); // Invalidate cache to reflect the changes - await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secretChangesCount: secretChanges.length, diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index fbd9d3fd6..185ab5e94 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -1,5 +1,6 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; +import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; @@ -27,6 +28,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { queueService: TQueueServiceFactory; orgService: TOrgServiceFactory; userNotificationDAL: Pick; + keyValueStoreDAL: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -43,7 +45,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ identityUniversalAuthClientSecretDAL, serviceTokenService, orgService, - userNotificationDAL + userNotificationDAL, + keyValueStoreDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { const appCfg = getConfig(); @@ -52,6 +55,10 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ } const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + await queueService.stopRepeatableJob( QueueName.AuditLogPrune, QueueJobs.AuditLogPrune, @@ -82,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await orgService.notifyInvitedUsers(); await auditLogDAL.pruneAuditLog(); await userNotificationDAL.pruneNotifications(); + await keyValueStoreDAL.pruneExpiredKeys(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); } catch (error) { logger.error(error, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 8d9c6958a..fb7101071 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -50,15 +50,14 @@ interface TSecretV2DalArg { } export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60); -export const SECRET_DAL_VERSION_TTL = 15 * 60; +export const SECRET_DAL_VERSION_TTL = "15m"; export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const secretOrm = ormify(db, TableName.SecretV2); - const invalidateSecretCacheByProjectId = async (projectId: string) => { + const invalidateSecretCacheByProjectId = async (projectId: string, tx?: Knex) => { const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId); - await keyStore.incrementBy(secretDalVersionKey, 1); - await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL); + await keyStore.pgIncrementBy(secretDalVersionKey, { incr: 1, tx, expiry: SECRET_DAL_VERSION_TTL }); }; const findOne = async (filter: Partial, tx?: Knex) => { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 48ab07816..9db535fbe 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -118,7 +118,7 @@ type TSecretV2BridgeServiceFactoryDep = { >; snapshotService: Pick; resourceMetadataDAL: Pick; - keyStore: Pick; + keyStore: Pick; reminderService: Pick; }; @@ -360,6 +360,7 @@ export const secretV2BridgeServiceFactory = ({ tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); return createdSecret; }); @@ -377,7 +378,6 @@ export const secretV2BridgeServiceFactory = ({ }); } - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -566,8 +566,8 @@ export const secretV2BridgeServiceFactory = ({ await $validateSecretReferences(projectId, permission, allSecretReferences); } - const updatedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const updatedSecret = await secretDAL.transaction(async (tx) => { + const modifiedSecretsInDB = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, resourceMetadataDAL, @@ -598,8 +598,11 @@ export const secretV2BridgeServiceFactory = ({ actorId }, tx - }) - ); + }); + + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; + }); if (inputSecret.secretReminderRepeatDays) { await reminderService.createReminder({ actor, @@ -615,7 +618,6 @@ export const secretV2BridgeServiceFactory = ({ }); } - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -715,8 +717,8 @@ export const secretV2BridgeServiceFactory = ({ ); try { - const deletedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ + const deletedSecret = await secretDAL.transaction(async (tx) => { + const modifiedSecretsInDB = await fnSecretBulkDelete({ projectId, folderId, actorId, @@ -732,10 +734,11 @@ export const secretV2BridgeServiceFactory = ({ } ], tx - }) - ); + }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; + }); - await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1027,7 +1030,7 @@ export const secretV2BridgeServiceFactory = ({ }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); - const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); + const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); const secretDalVersion = Number(cachedSecretDalVersion || 0); const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { ...dto, @@ -1692,7 +1695,7 @@ export const secretV2BridgeServiceFactory = ({ await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); const executeBulkInsert = async (tx: Knex) => { - return fnSecretBulkInsert({ + const modifiedSecretsInDB = await fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1728,13 +1731,14 @@ export const secretV2BridgeServiceFactory = ({ }, tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; }; const newSecrets = providedTx ? await executeBulkInsert(providedTx) : await secretDAL.transaction(executeBulkInsert); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ actor, @@ -2099,6 +2103,7 @@ export const secretV2BridgeServiceFactory = ({ } } + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); return updatedSecrets; }; @@ -2106,7 +2111,6 @@ export const secretV2BridgeServiceFactory = ({ ? await executeBulkUpdate(providedTx) : await secretDAL.transaction(executeBulkUpdate); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined))); await Promise.allSettled( folders.map((el) => @@ -2233,7 +2237,7 @@ export const secretV2BridgeServiceFactory = ({ }); const executeBulkDelete = async (tx: Knex) => { - return fnSecretBulkDelete({ + const modifiedSecretsInDB = await fnSecretBulkDelete({ secretDAL, secretQueueService, folderCommitService, @@ -2249,6 +2253,8 @@ export const secretV2BridgeServiceFactory = ({ commitChanges, tx }); + await secretDAL.invalidateSecretCacheByProjectId(projectId, tx); + return modifiedSecretsInDB; }; try { @@ -2256,7 +2262,6 @@ export const secretV2BridgeServiceFactory = ({ ? await executeBulkDelete(providedTx) : await secretDAL.transaction(executeBulkDelete); - await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ actor, From e8e51fb1e1211524bd5dd2ee449200f7c58feacf Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Sep 2025 14:28:37 +0530 Subject: [PATCH 2/6] feat: added back missing replica node redirect for read db operations --- .../dynamic-secret-lease/dynamic-secret-lease-dal.ts | 7 +++++-- backend/src/ee/services/gateway/gateway-dal.ts | 2 +- .../ee/services/group/user-group-membership-dal.ts | 4 ++-- backend/src/ee/services/license/license-dal.ts | 4 ++-- .../secret-approval-request-dal.ts | 4 ++-- .../secret-approval-request-secret-dal.ts | 2 +- .../src/ee/services/secret-snapshot/snapshot-dal.ts | 12 ++++++++---- .../integration-auth/integration-auth-dal.ts | 2 +- backend/src/services/kms/kms-root-config-dal.ts | 2 +- backend/src/services/reminder/reminder-dal.ts | 8 ++++---- .../secret-folder/secret-folder-version-dal.ts | 2 +- .../src/services/secret-import/secret-import-dal.ts | 2 +- .../services/secret-sharing/secret-sharing-dal.ts | 2 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 12 ++++++------ .../services/secret-v2-bridge/secret-version-dal.ts | 6 +++--- backend/src/services/super-admin/super-admin-dal.ts | 2 +- backend/src/services/user/user-dal.ts | 12 ++++++++---- 17 files changed, 48 insertions(+), 37 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 525de9efd..974e20061 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -46,7 +46,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first(); + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) + .count("*") + .where({ dynamicSecretId }) + .first(); return parseInt(doc || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); @@ -55,7 +58,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease) + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index 31b4b727b..c21ff31c0 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -13,7 +13,7 @@ export const gatewayDALFactory = (db: TDbClient) => { { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db)(TableName.Gateway) + const query = (tx || db.replicaNode())(TableName.Gateway) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.Gateway, ["orgId"])) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index 5ee97e457..374459b0c 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -23,7 +23,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) .pluck(`${TableName.ProjectMembership}.projectId`); - const userGroupMemberships: string[] = await (tx || db)(TableName.UserGroupMembership) + const userGroupMemberships: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId) .join( @@ -79,7 +79,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { .pluck(`${TableName.GroupProjectMembership}.groupId`); // main query - const members = await (tx || db)(TableName.UserGroupMembership) + const members = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.groupId`, groupId) .where(`${TableName.UserGroupMembership}.isPending`, false) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index 88a2dadf6..cfea2573d 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -28,7 +28,7 @@ export const licenseDALFactory = (db: TDbClient) => { const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users - const userDoc = await (tx || db)(TableName.OrgMembership) + const userDoc = await (tx || db.replicaNode())(TableName.OrgMembership) .where({ status: OrgMembershipStatus.Accepted }) .andWhere((bd) => { if (orgId) { @@ -42,7 +42,7 @@ export const licenseDALFactory = (db: TDbClient) => { const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db)(TableName.IdentityOrgMembership) + const identityDoc = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) .where((bd) => { if (orgId) { void bd.where({ orgId }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index fe4ca94e1..01caef223 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -345,7 +345,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { const findProjectRequestCount = async (projectId: string, userId: string, policyId?: string, tx?: Knex) => { try { - const docs = await (tx || db) + const docs = await (tx || db.replicaNode()) .with( "temp", (tx || db.replicaNode())(TableName.SecretApprovalRequest) @@ -494,7 +494,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .distinctOn(`${TableName.SecretApprovalRequest}.id`) .as("inner"); - const query = (tx || db) + const query = (tx || db.replicaNode()) .select("*") .select(db.raw("count(*) OVER() as total_count")) .from(innerQuery) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index c1b18e43d..17182cddf 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -377,7 +377,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { // special query for migration to v2 secret const findByProjectId = async (projectId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretApprovalRequestSecret) + const docs = await (tx || db.replicaNode())(TableName.SecretApprovalRequestSecret) .join( TableName.SecretApprovalRequest, `${TableName.SecretApprovalRequest}.id`, diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index c547d85c2..17f1fad05 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -265,7 +265,7 @@ export const snapshotDALFactory = (db: TDbClient) => { // then joins with respective secrets and folder const findRecursivelySnapshots = async (snapshotId: string, tx?: Knex) => { try { - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .withRecursive("parent", (qb) => { void qb .from(TableName.Snapshot) @@ -419,7 +419,7 @@ export const snapshotDALFactory = (db: TDbClient) => { // then joins with respective secrets and folder const findRecursivelySnapshotsV2Bridge = async (snapshotId: string, tx?: Knex) => { try { - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .withRecursive("parent", (qb) => { void qb .from(TableName.Snapshot) @@ -581,7 +581,11 @@ export const snapshotDALFactory = (db: TDbClient) => { const docs = await (tx || db.replicaNode())(TableName.Snapshot) .where(`${TableName.Snapshot}.folderId`, folderId) .join( - (tx || db)(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"), + (tx || db.replicaNode())(TableName.Snapshot) + .groupBy("folderId") + .max("createdAt") + .select("folderId") + .as("latestVersion"), (bd) => { bd.on(`${TableName.Snapshot}.folderId`, "latestVersion.folderId").andOn( `${TableName.Snapshot}.createdAt`, @@ -766,7 +770,7 @@ export const snapshotDALFactory = (db: TDbClient) => { ) .orderBy(`${TableName.Snapshot}.createdAt`, "desc") .where(`${TableName.Snapshot}.folderId`, folderId); - const data = await (tx || db) + const data = await (tx || db.replicaNode()) .with("w", query) .select("*") .from[number]>("w") diff --git a/backend/src/services/integration-auth/integration-auth-dal.ts b/backend/src/services/integration-auth/integration-auth-dal.ts index 7a56afcbb..d3ccf610b 100644 --- a/backend/src/services/integration-auth/integration-auth-dal.ts +++ b/backend/src/services/integration-auth/integration-auth-dal.ts @@ -30,7 +30,7 @@ export const integrationAuthDALFactory = (db: TDbClient) => { const getByOrg = async (orgId: string, tx?: Knex) => { try { - const integrationAuths = await (tx || db)(TableName.IntegrationAuth) + const integrationAuths = await (tx || db.replicaNode())(TableName.IntegrationAuth) .join(TableName.Project, `${TableName.Project}.id`, `${TableName.IntegrationAuth}.projectId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .where(`${TableName.Organization}.id`, "=", orgId) diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index 31826b79d..4de3fc15a 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -12,7 +12,7 @@ export const kmsRootConfigDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const result = await (tx || db)(TableName.KmsServerRootConfig) + const result = await (tx || db.replicaNode())(TableName.KmsServerRootConfig) .where({ id } as never) .first("*"); return result; diff --git a/backend/src/services/reminder/reminder-dal.ts b/backend/src/services/reminder/reminder-dal.ts index 897a75234..4161552a9 100644 --- a/backend/src/services/reminder/reminder-dal.ts +++ b/backend/src/services/reminder/reminder-dal.ts @@ -39,7 +39,7 @@ export const reminderDALFactory = (db: TDbClient) => { const findSecretDailyReminders = async (tx?: Knex) => { const { startOfDay, endOfDay } = getTodayDateRange(); - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .whereBetween("nextReminderDate", [startOfDay, endOfDay]) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .leftJoin(TableName.Users, `${TableName.ReminderRecipient}.userId`, `${TableName.Users}.id`) @@ -90,7 +90,7 @@ export const reminderDALFactory = (db: TDbClient) => { const futureDate = new Date(startOfDay); futureDate.setDate(futureDate.getDate() + daysAhead); - const reminders = await (tx || db)(TableName.Reminder) + const reminders = await (tx || db.replicaNode())(TableName.Reminder) .where("nextReminderDate", ">=", startOfDay) .where("nextReminderDate", "<=", futureDate) .orderBy("nextReminderDate", "asc") @@ -101,7 +101,7 @@ export const reminderDALFactory = (db: TDbClient) => { }; const findSecretReminder = async (secretId: string, tx?: Knex) => { - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .where(`${TableName.Reminder}.secretId`, secretId) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .select(selectAllTableCols(TableName.Reminder)) @@ -125,7 +125,7 @@ export const reminderDALFactory = (db: TDbClient) => { }; const findSecretReminders = async (secretIds: string[], tx?: Knex) => { - const rawReminders = await (tx || db)(TableName.Reminder) + const rawReminders = await (tx || db.replicaNode())(TableName.Reminder) .whereIn(`${TableName.Reminder}.secretId`, secretIds) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) .select(selectAllTableCols(TableName.Reminder)) diff --git a/backend/src/services/secret-folder/secret-folder-version-dal.ts b/backend/src/services/secret-folder/secret-folder-version-dal.ts index 46ff49692..5504c6e0b 100644 --- a/backend/src/services/secret-folder/secret-folder-version-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-version-dal.ts @@ -45,7 +45,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { ) .whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds) .join( - (tx || db)(TableName.SecretFolderVersion) + (tx || db.replicaNode())(TableName.SecretFolderVersion) .groupBy("folderId") .max("version") .select("folderId") diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index db611dc6c..2261c2418 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -15,7 +15,7 @@ export const secretImportDALFactory = (db: TDbClient) => { // we are using postion based sorting as its a small list // this will return the last value of the position in a folder with secret imports const findLastImportPosition = async (folderId: string, tx?: Knex) => { - const lastPos = await (tx || db)(TableName.SecretImport) + const lastPos = await (tx || db.replicaNode())(TableName.SecretImport) .where({ folderId }) .max("position", { as: "position" }) .first(); diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 7cdccd4f8..08e0ad257 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -119,7 +119,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { const findActiveSharedSecrets = async (filters: Partial, tx?: Knex) => { try { const now = new Date(); - return await (tx || db)(TableName.SecretSharing) + return await (tx || db.replicaNode())(TableName.SecretSharing) .where(filters) .andWhere("expiresAt", ">", now) .andWhere("encryptedValue", "<>", "") diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index fb7101071..93afb3b55 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -62,7 +62,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.SecretV2)) .leftJoin( @@ -143,7 +143,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const find = async (filter: TFindFilter, opts: TFindOpt = {}) => { const { offset, limit, sort, tx } = opts; try { - const query = (tx || db)(TableName.SecretV2) + const query = (tx || db.replicaNode())(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter)) .leftJoin( @@ -887,13 +887,13 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const findSecretsWithReminderRecipients = async (ids: string[], limit: number, tx?: Knex) => { try { // Create a subquery to get limited secret IDs - const limitedSecretIds = (tx || db)(TableName.SecretV2) + const limitedSecretIds = (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, ids) .limit(limit) .select("id"); // Join with all recipients for the limited secrets - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) .leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`) @@ -925,13 +925,13 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => { try { // Create a subquery to get limited secret IDs - const limitedSecretIds = (tx || db)(TableName.SecretV2) + const limitedSecretIds = (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, ids) .limit(limit) .select("id"); // Join with all recipients for the limited secrets - const docs = await (tx || db)(TableName.SecretV2) + const docs = await (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) .leftJoin( diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 9537b79e3..0282fa537 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -72,7 +72,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .where(`${TableName.SecretVersionV2}.folderId`, folderId) .join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`) .join( - (tx || db)(TableName.SecretVersionV2) + (tx || db.replicaNode())(TableName.SecretVersionV2) .where(`${TableName.SecretVersionV2}.folderId`, folderId) .groupBy("secretId") .max("version") @@ -121,7 +121,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .where("folderId", folderId) .whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds) .join( - (tx || db)(TableName.SecretVersionV2) + (tx || db.replicaNode())(TableName.SecretVersionV2) .groupBy("secretId") .max("version") .select("secretId") @@ -189,7 +189,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { }) => { try { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; - const query = (tx || db)(TableName.SecretVersionV2) + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) .leftJoin( TableName.ProjectMembership, diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index d7d11a5d2..571583cc3 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -11,7 +11,7 @@ export const superAdminDALFactory = (db: TDbClient) => { const superAdminOrm = ormify(db, TableName.SuperAdmin); const findById = async (id: string, tx?: Knex) => { - const config = await (tx || db)(TableName.SuperAdmin) + const config = await (tx || db.replicaNode())(TableName.SuperAdmin) .where(`${TableName.SuperAdmin}.id`, id) .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) .leftJoin(TableName.SamlConfig, (qb) => { diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 0f623dff1..4267d13ee 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -19,12 +19,16 @@ export type TUserDALFactory = ReturnType; export const userDALFactory = (db: TDbClient) => { const userOrm = ormify(db, TableName.Users); const findUserByUsername = async (username: string, tx?: Knex) => - (tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() }); + (tx || db.replicaNode())(TableName.Users).whereRaw('lower("username") = :username', { + username: username.toLowerCase() + }); const findUserByEmail = async (email: string, tx?: Knex) => - (tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }).where({ - isEmailVerified: true - }); + (tx || db.replicaNode())(TableName.Users) + .whereRaw('lower("email") = :email', { email: email.toLowerCase() }) + .where({ + isEmailVerified: true + }); const getUsersByFilter = async ({ limit, From ae42f31d694c43ff1b3f57bf0b419428d412d1f1 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Sep 2025 21:50:14 +0530 Subject: [PATCH 3/6] feat: added replica node for auth method fetch --- api_requests.sh | 89 +++++++++++++++++++ backend/src/services/identity/identity-dal.ts | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100755 api_requests.sh diff --git a/api_requests.sh b/api_requests.sh new file mode 100755 index 000000000..5d21f114f --- /dev/null +++ b/api_requests.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Configuration +BEARER_TOKEN="your-token-here" +BASE_URL="https://api.example.com" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo "Starting API requests with timing metrics..." +echo "==========================================" + +# Request 1: POST with body +echo -e "\n${BLUE}1. POST Request${NC}" +echo "URL: $BASE_URL/endpoint1" +start_time=$(date +%s.%3N) +response1=$(curl -s -w "%{http_code}|%{time_total}" \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "key1": "value1", + "key2": "value2" + }' \ + "$BASE_URL/endpoint1") +end_time=$(date +%s.%3N) + +http_code1=$(echo "$response1" | tail -c 10 | cut -d'|' -f1) +curl_time1=$(echo "$response1" | tail -c 10 | cut -d'|' -f2) +duration1=$(echo "$end_time - $start_time" | bc) + +echo "Status Code: $http_code1" +echo "Duration: ${duration1}s (curl time: ${curl_time1}s)" +echo -e "${GREEN}✓ POST request completed${NC}" + +# Request 2: GET with query params +echo -e "\n${BLUE}2. GET Request with Query Parameters${NC}" +echo "URL: $BASE_URL/endpoint2?param1=value1¶m2=value2" +start_time=$(date +%s.%3N) +response2=$(curl -s -w "%{http_code}|%{time_total}" \ + -X GET \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + "$BASE_URL/endpoint2?param1=value1¶m2=value2") +end_time=$(date +%s.%3N) + +http_code2=$(echo "$response2" | tail -c 10 | cut -d'|' -f1) +curl_time2=$(echo "$response2" | tail -c 10 | cut -d'|' -f2) +duration2=$(echo "$end_time - $start_time" | bc) + +echo "Status Code: $http_code2" +echo "Duration: ${duration2}s (curl time: ${curl_time2}s)" +echo -e "${GREEN}✓ GET request completed${NC}" + +# Request 3: DELETE with body +echo -e "\n${BLUE}3. DELETE Request with Body${NC}" +echo "URL: $BASE_URL/endpoint3" +start_time=$(date +%s.%3N) +response3=$(curl -s -w "%{http_code}|%{time_total}" \ + -X DELETE \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "123", + "reason": "cleanup" + }' \ + "$BASE_URL/endpoint3") +end_time=$(date +%s.%3N) + +http_code3=$(echo "$response3" | tail -c 10 | cut -d'|' -f1) +curl_time3=$(echo "$response3" | tail -c 10 | cut -d'|' -f2) +duration3=$(echo "$end_time - $start_time" | bc) + +echo "Status Code: $http_code3" +echo "Duration: ${duration3}s (curl time: ${curl_time3}s)" +echo -e "${GREEN}✓ DELETE request completed${NC}" + +# Summary +echo -e "\n==========================================" +echo -e "${BLUE}SUMMARY${NC}" +echo "==========================================" +echo "Request 1 (POST): ${duration1}s" +echo "Request 2 (GET): ${duration2}s" +echo "Request 3 (DELETE): ${duration3}s" +total_time=$(echo "$duration1 + $duration2 + $duration3" | bc) +echo "Total Time: ${total_time}s" +echo -e "${GREEN}All requests completed successfully!${NC}" \ No newline at end of file diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 363412493..7bc797600 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -25,7 +25,7 @@ export const identityDALFactory = (db: TDbClient) => { } as const; const tableName = authMethodToTableName[authMethod]; if (!tableName) return; - const data = await db(tableName).where({ identityId }).first(); + const data = await db.replicaNode()(tableName).where({ identityId }).first(); if (!data) return; return data.accessTokenTrustedIps; }; From c682d87d959b4221e653a9a6f14afb028b4b1af0 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Sep 2025 22:55:32 +0530 Subject: [PATCH 4/6] feat: reptile review fixed --- .../secret-approval-request-service.ts | 1 + backend/src/keystore/key-value-store-dal.ts | 6 +++--- backend/src/lib/knex/index.ts | 2 +- .../services/folder-commit/folder-commit-service.test.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index aa3e3dc0f..17b7d8347 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -787,6 +787,7 @@ export const secretApprovalRequestServiceFactory = ({ }, tx ); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx); return { secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret }, approval: updatedSecretApproval diff --git a/backend/src/keystore/key-value-store-dal.ts b/backend/src/keystore/key-value-store-dal.ts index bdf5e8725..bccedf4ac 100644 --- a/backend/src/keystore/key-value-store-dal.ts +++ b/backend/src/keystore/key-value-store-dal.ts @@ -19,7 +19,7 @@ const MAX_RETRY_ON_FAILURE = 3; export const keyValueStoreDALFactory = (db: TDbClient): TKeyValueStoreDALFactory => { const keyValueStoreOrm = ormify(db, TableName.KeyValueStore); - const incrementBy: TKeyValueStoreDALFactory["incrementBy"] = (key, { incr = 1, tx, expiresAt }) => { + const incrementBy: TKeyValueStoreDALFactory["incrementBy"] = async (key, { incr = 1, tx, expiresAt }) => { return (tx || db)(TableName.KeyValueStore) .insert({ key, integerValue: 1, expiresAt }) .onConflict("key") @@ -27,7 +27,8 @@ export const keyValueStoreDALFactory = (db: TDbClient): TKeyValueStoreDALFactory integerValue: db.raw(`"${TableName.KeyValueStore}"."integerValue" + ?`, [incr]), expiresAt }) - .returning("integerValue"); + .returning("integerValue") + .then((result) => Number(result[0]?.integerValue || 0)); }; const findOneInt: TKeyValueStoreDALFactory["findOneInt"] = async (key, tx) => { @@ -44,7 +45,6 @@ export const keyValueStoreDALFactory = (db: TDbClient): TKeyValueStoreDALFactory return Number(doc?.integerValue || 0); }; - // delete all audit log that have expired const pruneExpiredKeys: TKeyValueStoreDALFactory["pruneExpiredKeys"] = async () => { let deletedIds: { key: string }[] = []; let numberOfRetryOnFailure = 0; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index dbfd29b81..499e7cb26 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -278,7 +278,7 @@ export const ormify = ( } if ($decr) { Object.entries($decr).forEach(([incrementField, incrementValue]) => { - void query.increment(incrementField, incrementValue as number); + void query.decrement(incrementField, incrementValue as number); }); } return (await query) as Tables[Tname]["base"][]; diff --git a/backend/src/services/folder-commit/folder-commit-service.test.ts b/backend/src/services/folder-commit/folder-commit-service.test.ts index 28d603829..0a73d6e0c 100644 --- a/backend/src/services/folder-commit/folder-commit-service.test.ts +++ b/backend/src/services/folder-commit/folder-commit-service.test.ts @@ -661,7 +661,7 @@ describe("folderCommitServiceFactory", () => { // Assert expect(mockFolderCommitDAL.create).toHaveBeenCalled(); - expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId); + expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId, {}); // Check that we got the right counts expect(result.totalChanges).toEqual(2); From 6f1c884bf5f3be08f2918cb0129f84fd828b98a9 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Sep 2025 16:11:20 +0530 Subject: [PATCH 5/6] feat: review comments --- api_requests.sh | 89 ------------------- .../20250908193226_sql-cache_int.ts | 1 + backend/src/db/schemas/key-value-store.ts | 4 +- 3 files changed, 4 insertions(+), 90 deletions(-) delete mode 100755 api_requests.sh diff --git a/api_requests.sh b/api_requests.sh deleted file mode 100755 index 5d21f114f..000000000 --- a/api_requests.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Configuration -BEARER_TOKEN="your-token-here" -BASE_URL="https://api.example.com" - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -echo "Starting API requests with timing metrics..." -echo "==========================================" - -# Request 1: POST with body -echo -e "\n${BLUE}1. POST Request${NC}" -echo "URL: $BASE_URL/endpoint1" -start_time=$(date +%s.%3N) -response1=$(curl -s -w "%{http_code}|%{time_total}" \ - -X POST \ - -H "Authorization: Bearer $BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "key1": "value1", - "key2": "value2" - }' \ - "$BASE_URL/endpoint1") -end_time=$(date +%s.%3N) - -http_code1=$(echo "$response1" | tail -c 10 | cut -d'|' -f1) -curl_time1=$(echo "$response1" | tail -c 10 | cut -d'|' -f2) -duration1=$(echo "$end_time - $start_time" | bc) - -echo "Status Code: $http_code1" -echo "Duration: ${duration1}s (curl time: ${curl_time1}s)" -echo -e "${GREEN}✓ POST request completed${NC}" - -# Request 2: GET with query params -echo -e "\n${BLUE}2. GET Request with Query Parameters${NC}" -echo "URL: $BASE_URL/endpoint2?param1=value1¶m2=value2" -start_time=$(date +%s.%3N) -response2=$(curl -s -w "%{http_code}|%{time_total}" \ - -X GET \ - -H "Authorization: Bearer $BEARER_TOKEN" \ - "$BASE_URL/endpoint2?param1=value1¶m2=value2") -end_time=$(date +%s.%3N) - -http_code2=$(echo "$response2" | tail -c 10 | cut -d'|' -f1) -curl_time2=$(echo "$response2" | tail -c 10 | cut -d'|' -f2) -duration2=$(echo "$end_time - $start_time" | bc) - -echo "Status Code: $http_code2" -echo "Duration: ${duration2}s (curl time: ${curl_time2}s)" -echo -e "${GREEN}✓ GET request completed${NC}" - -# Request 3: DELETE with body -echo -e "\n${BLUE}3. DELETE Request with Body${NC}" -echo "URL: $BASE_URL/endpoint3" -start_time=$(date +%s.%3N) -response3=$(curl -s -w "%{http_code}|%{time_total}" \ - -X DELETE \ - -H "Authorization: Bearer $BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "id": "123", - "reason": "cleanup" - }' \ - "$BASE_URL/endpoint3") -end_time=$(date +%s.%3N) - -http_code3=$(echo "$response3" | tail -c 10 | cut -d'|' -f1) -curl_time3=$(echo "$response3" | tail -c 10 | cut -d'|' -f2) -duration3=$(echo "$end_time - $start_time" | bc) - -echo "Status Code: $http_code3" -echo "Duration: ${duration3}s (curl time: ${curl_time3}s)" -echo -e "${GREEN}✓ DELETE request completed${NC}" - -# Summary -echo -e "\n==========================================" -echo -e "${BLUE}SUMMARY${NC}" -echo "==========================================" -echo "Request 1 (POST): ${duration1}s" -echo "Request 2 (GET): ${duration2}s" -echo "Request 3 (DELETE): ${duration3}s" -total_time=$(echo "$duration1 + $duration2 + $duration3" | bc) -echo "Total Time: ${total_time}s" -echo -e "${GREEN}All requests completed successfully!${NC}" \ No newline at end of file diff --git a/backend/src/db/migrations/20250908193226_sql-cache_int.ts b/backend/src/db/migrations/20250908193226_sql-cache_int.ts index dc4060d02..0e15c10d1 100644 --- a/backend/src/db/migrations/20250908193226_sql-cache_int.ts +++ b/backend/src/db/migrations/20250908193226_sql-cache_int.ts @@ -8,6 +8,7 @@ export async function up(knex: Knex): Promise { t.text("key").primary(); t.bigint("integerValue"); t.datetime("expiresAt"); + t.timestamps(true, true, true); }); } } diff --git a/backend/src/db/schemas/key-value-store.ts b/backend/src/db/schemas/key-value-store.ts index d6ba90b2f..448c78f24 100644 --- a/backend/src/db/schemas/key-value-store.ts +++ b/backend/src/db/schemas/key-value-store.ts @@ -10,7 +10,9 @@ import { TImmutableDBKeys } from "./models"; export const KeyValueStoreSchema = z.object({ key: z.string(), integerValue: z.coerce.number().nullable().optional(), - expiresAt: z.date().nullable().optional() + expiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() }); export type TKeyValueStore = z.infer; From 0153cf946713643091b19538948c41148bb147c9 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Sep 2025 22:55:15 +0530 Subject: [PATCH 6/6] fix: resolved validate schema failing in pipeline --- backend/src/services/kms/kms-root-config-dal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index 4de3fc15a..1b9b9a230 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -12,7 +12,7 @@ export const kmsRootConfigDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const result = await (tx || db.replicaNode())(TableName.KmsServerRootConfig) + const result = await (tx || db?.replicaNode?.() || db)(TableName.KmsServerRootConfig) .where({ id } as never) .first("*"); return result;