mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
PIT: add backend logic for deep PIT and rollback
This commit is contained in:
@@ -13,10 +13,12 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.string("actorType").notNullable();
|
t.string("actorType").notNullable();
|
||||||
t.string("message");
|
t.string("message");
|
||||||
t.uuid("folderId").notNullable();
|
t.uuid("folderId").notNullable();
|
||||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
t.uuid("envId").notNullable();
|
||||||
|
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
t.index("folderId");
|
t.index("folderId");
|
||||||
|
t.index("envId");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("folderTreeCheckpointId").notNullable();
|
t.uuid("folderTreeCheckpointId").notNullable();
|
||||||
t.foreign("folderTreeCheckpointId").references("id").inTable(TableName.FolderTreeCheckpoint).onDelete("CASCADE");
|
t.foreign("folderTreeCheckpointId").references("id").inTable(TableName.FolderTreeCheckpoint).onDelete("CASCADE");
|
||||||
t.uuid("folderId").notNullable();
|
t.uuid("folderId").notNullable();
|
||||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
|
||||||
t.uuid("folderCommitId").notNullable();
|
t.uuid("folderCommitId").notNullable();
|
||||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commi
|
|||||||
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
@@ -76,6 +77,7 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
|
|||||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||||
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||||
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||||
|
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||||
|
|
||||||
const folderCommitService = folderCommitServiceFactory({
|
const folderCommitService = folderCommitServiceFactory({
|
||||||
folderCommitDAL,
|
folderCommitDAL,
|
||||||
@@ -89,7 +91,8 @@ export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyS
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderCheckpointResourcesDAL,
|
folderCheckpointResourcesDAL,
|
||||||
secretV2BridgeDAL
|
secretV2BridgeDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
return { folderCommitService };
|
return { folderCommitService };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const FolderCommitsSchema = z.object({
|
|||||||
actorType: z.string(),
|
actorType: z.string(),
|
||||||
message: z.string().nullable().optional(),
|
message: z.string().nullable().optional(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
|
envId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,52 @@ import { z } from "zod";
|
|||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
|
||||||
export const registerPITRouter = async (server: FastifyZodProvider) => {
|
export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||||
|
// Get all commits for a folder
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/diff",
|
url: "/commits/:folderId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
folderId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const commits = await server.services.folderCommit.getCommitsByFolderId(req.params.folderId);
|
||||||
|
return commits;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get commit changes for a specific commit
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits/:commitId/changes",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const changes = await server.services.folderCommit.getCommitChanges(req.params.commitId);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare folder states between commits
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/compare",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -19,12 +62,16 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const backup = await server.services.folderCommit.compareFolderStates(req.query.fromCommit, req.query.toCommit);
|
const diff = await server.services.folderCommit.compareFolderStates({
|
||||||
|
currentCommitId: req.query.fromCommit,
|
||||||
|
targetCommitId: req.query.toCommit
|
||||||
|
});
|
||||||
|
|
||||||
return backup;
|
return diff;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rollback to a previous commit
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/rollback",
|
url: "/rollback",
|
||||||
@@ -36,6 +83,46 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
fromCommit: z.string().trim(),
|
fromCommit: z.string().trim(),
|
||||||
toCommit: z.string().trim(),
|
toCommit: z.string().trim(),
|
||||||
folderId: z.string().trim(),
|
folderId: z.string().trim(),
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
reconstructNewFolders: z.boolean().default(false)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const diff = await server.services.folderCommit.compareFolderStates({
|
||||||
|
currentCommitId: req.body.fromCommit,
|
||||||
|
targetCommitId: req.body.toCommit
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await server.services.folderCommit.applyFolderStateDifferences({
|
||||||
|
differences: diff,
|
||||||
|
actorInfo: {
|
||||||
|
actorType: req.permission?.type || "PLATFORM",
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
message: "Rollback to previous commit"
|
||||||
|
},
|
||||||
|
folderId: req.body.folderId,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
reconstructNewFolders: req.body.reconstructNewFolders
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deep rollback to a specific commit
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/deep-rollback",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
commitId: z.string().trim(),
|
||||||
|
envId: z.string().trim(),
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -43,19 +130,15 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const diff = await server.services.folderCommit.compareFolderStates(req.body.fromCommit, req.body.toCommit);
|
await server.services.folderCommit.deepRollbackFolder(
|
||||||
const response = await server.services.folderCommit.applyFolderStateDifferences(
|
req.body.commitId,
|
||||||
diff,
|
req.body.envId,
|
||||||
{
|
req.permission?.id || "PLATFORM",
|
||||||
actorType: req.permission?.type || "PLATFORM",
|
req.permission?.type || "PLATFORM",
|
||||||
actorId: req.permission?.id,
|
|
||||||
message: "Rollback to previous commit"
|
|
||||||
},
|
|
||||||
req.body.folderId,
|
|
||||||
req.body.projectId
|
req.body.projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return { success: true };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ const envSchema = z
|
|||||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||||
|
|
||||||
// PIT
|
// PIT
|
||||||
CHECKPOINT_WINDOW: zpStr(z.string().optional().default("10")),
|
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("10")),
|
||||||
|
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")),
|
||||||
|
|
||||||
/* CORS ----------------------------------------------------------------------------- */
|
/* CORS ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export enum QueueName {
|
|||||||
AccessTokenStatusUpdate = "access-token-status-update",
|
AccessTokenStatusUpdate = "access-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
SecretRotationV2 = "secret-rotation-v2"
|
SecretRotationV2 = "secret-rotation-v2",
|
||||||
|
FolderTreeCheckpoint = "folder-tree-checkpoint"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -81,7 +82,8 @@ export enum QueueJobs {
|
|||||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
|
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -191,6 +193,12 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.ProjectV3Migration;
|
name: QueueJobs.ProjectV3Migration;
|
||||||
payload: { projectId: string };
|
payload: { projectId: string };
|
||||||
};
|
};
|
||||||
|
[QueueName.FolderTreeCheckpoint]: {
|
||||||
|
name: QueueJobs.CreateFolderTreeCheckpoint;
|
||||||
|
payload: {
|
||||||
|
envId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.ImportSecretsFromExternalSource]: {
|
[QueueName.ImportSecretsFromExternalSource]: {
|
||||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -143,9 +143,11 @@ import { externalMigrationServiceFactory } from "@app/services/external-migratio
|
|||||||
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
||||||
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||||
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
||||||
|
import { folderCommitQueueServiceFactory } from "@app/services/folder-commit/folder-commit-queue";
|
||||||
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
@@ -563,6 +565,14 @@ export const registerRoutes = async (
|
|||||||
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||||
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
||||||
const folderCommitDAL = folderCommitDALFactory(db);
|
const folderCommitDAL = folderCommitDALFactory(db);
|
||||||
|
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||||
|
const folderCommitQueueService = folderCommitQueueServiceFactory({
|
||||||
|
queueService,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitDAL,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
const folderCommitService = folderCommitServiceFactory({
|
const folderCommitService = folderCommitServiceFactory({
|
||||||
folderCommitDAL,
|
folderCommitDAL,
|
||||||
folderCommitChangesDAL,
|
folderCommitChangesDAL,
|
||||||
@@ -575,7 +585,9 @@ export const registerRoutes = async (
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderCheckpointResourcesDAL,
|
folderCheckpointResourcesDAL,
|
||||||
secretV2BridgeDAL
|
secretV2BridgeDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitQueueService
|
||||||
});
|
});
|
||||||
const scimService = scimServiceFactory({
|
const scimService = scimServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
|
|||||||
@@ -42,6 +42,75 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findLatestCommitByFolderIds = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[] | undefined> => {
|
||||||
|
try {
|
||||||
|
// First get max commitId for each folderId
|
||||||
|
const maxCommitIdSubquery = (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.select("folderId")
|
||||||
|
.max("commitId as maxCommitId")
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.groupBy("folderId");
|
||||||
|
|
||||||
|
// Join with main table to get complete records for each max commitId
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
.join<TFolderCommits>(maxCommitIdSubquery.as("latest"), function () {
|
||||||
|
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
|
||||||
|
`${TableName.FolderCommit}.commitId`,
|
||||||
|
"=",
|
||||||
|
"latest.maxCommitId"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitByFolderIds" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestEnvCommit = async (envId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommit" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMultipleLatestCommits = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[]> => {
|
||||||
|
try {
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// Get the latest commitId for each folderId
|
||||||
|
const subquery = knexInstance(TableName.FolderCommit)
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.groupBy("folderId")
|
||||||
|
.select("folderId")
|
||||||
|
.max("commitId as maxCommitId");
|
||||||
|
|
||||||
|
// Then fetch the complete rows matching those latest commits
|
||||||
|
const docs = await knexInstance(TableName.FolderCommit)
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
.innerJoin<TFolderCommits>(subquery.as("latest"), function () {
|
||||||
|
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
|
||||||
|
`${TableName.FolderCommit}.commitId`,
|
||||||
|
"=",
|
||||||
|
"latest.maxCommitId"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit));
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindMultipleLatestCommits" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getNumberOfCommitsSince = async (folderId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
|
const getNumberOfCommitsSince = async (folderId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
@@ -62,6 +131,26 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEnvNumberOfCommitsSince = async (envId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ id: folderCommitId })
|
||||||
|
.select("commitId")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (referencedCommit?.commitId) {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.where("commitId", ">", referencedCommit.commitId)
|
||||||
|
.count();
|
||||||
|
return Number(doc?.[0].count);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findCommitsToRecreate = async (
|
const findCommitsToRecreate = async (
|
||||||
folderId: string,
|
folderId: string,
|
||||||
targetCommitNumber: number,
|
targetCommitNumber: number,
|
||||||
@@ -131,11 +220,77 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findLatestCommitBetween = async ({
|
||||||
|
folderId,
|
||||||
|
startCommitId,
|
||||||
|
endCommitId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
folderId: string;
|
||||||
|
startCommitId?: string;
|
||||||
|
endCommitId: string;
|
||||||
|
tx?: Knex;
|
||||||
|
}): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where("commitId", "<=", endCommitId)
|
||||||
|
.where({ folderId })
|
||||||
|
.where((qb) => {
|
||||||
|
if (startCommitId) {
|
||||||
|
void qb.where("commitId", ">=", startCommitId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllCommitsBetween = async ({
|
||||||
|
envId,
|
||||||
|
startCommitId,
|
||||||
|
endCommitId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
folderId?: string;
|
||||||
|
envId?: string;
|
||||||
|
startCommitId?: string;
|
||||||
|
endCommitId: string;
|
||||||
|
tx?: Knex;
|
||||||
|
}): Promise<TFolderCommits[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where("commitId", "<=", endCommitId)
|
||||||
|
.where((qb) => {
|
||||||
|
if (envId) {
|
||||||
|
void qb.where(`${TableName.FolderCommit}.envId`, "=", envId);
|
||||||
|
}
|
||||||
|
if (startCommitId) {
|
||||||
|
void qb.where("commitId", ">=", startCommitId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc");
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...restOfOrm,
|
...restOfOrm,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
findLatestCommit,
|
findLatestCommit,
|
||||||
getNumberOfCommitsSince,
|
getNumberOfCommitsSince,
|
||||||
findCommitsToRecreate
|
findCommitsToRecreate,
|
||||||
|
findMultipleLatestCommits,
|
||||||
|
findAllCommitsBetween,
|
||||||
|
findLatestCommitBetween,
|
||||||
|
findLatestEnvCommit,
|
||||||
|
getEnvNumberOfCommitsSince,
|
||||||
|
findLatestCommitByFolderIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { TSecretFolders } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TFolderCommitDALFactory } from "./folder-commit-dal";
|
||||||
|
|
||||||
|
type TFolderCommitQueueServiceFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
folderTreeCheckpointDAL: Pick<
|
||||||
|
TFolderTreeCheckpointDALFactory,
|
||||||
|
"create" | "findLatestByEnvId" | "findNearestCheckpoint"
|
||||||
|
>;
|
||||||
|
folderTreeCheckpointResourcesDAL: Pick<
|
||||||
|
TFolderTreeCheckpointResourcesDALFactory,
|
||||||
|
"insertMany" | "findByTreeCheckpointId"
|
||||||
|
>;
|
||||||
|
folderCommitDAL: Pick<
|
||||||
|
TFolderCommitDALFactory,
|
||||||
|
"findLatestEnvCommit" | "getEnvNumberOfCommitsSince" | "findMultipleLatestCommits"
|
||||||
|
>;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByEnvId">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFolderCommitQueueServiceFactory = ReturnType<typeof folderCommitQueueServiceFactory>;
|
||||||
|
|
||||||
|
export const folderCommitQueueServiceFactory = ({
|
||||||
|
queueService,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitDAL,
|
||||||
|
folderDAL
|
||||||
|
}: TFolderCommitQueueServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const scheduleTreeCheckpoint = async (envId: string) => {
|
||||||
|
await queueService.queue(
|
||||||
|
QueueName.FolderTreeCheckpoint,
|
||||||
|
QueueJobs.CreateFolderTreeCheckpoint,
|
||||||
|
{ envId },
|
||||||
|
{
|
||||||
|
jobId: envId,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnFail: {
|
||||||
|
count: 3
|
||||||
|
},
|
||||||
|
removeOnComplete: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulePeriodicTreeCheckpoint = async (envId: string, intervalMs: number) => {
|
||||||
|
await queueService.queue(
|
||||||
|
QueueName.FolderTreeCheckpoint,
|
||||||
|
QueueJobs.CreateFolderTreeCheckpoint,
|
||||||
|
{ envId },
|
||||||
|
{
|
||||||
|
jobId: `periodic-${envId}`,
|
||||||
|
repeat: {
|
||||||
|
every: intervalMs
|
||||||
|
},
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnFail: false,
|
||||||
|
removeOnComplete: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelScheduledTreeCheckpoint = async (envId: string) => {
|
||||||
|
await queueService.stopJobById(QueueName.FolderTreeCheckpoint, envId);
|
||||||
|
await queueService.stopRepeatableJobByJobId(QueueName.FolderTreeCheckpoint, `periodic-${envId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort folders by hierarchy (copied from the source code)
|
||||||
|
const sortFoldersByHierarchy = (folders: TSecretFolders[]) => {
|
||||||
|
const childrenMap = new Map<string, TSecretFolders[]>();
|
||||||
|
const allFolderIds = new Set<string>();
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.id) allFolderIds.add(folder.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.parentId) {
|
||||||
|
const children = childrenMap.get(folder.parentId) || [];
|
||||||
|
children.push(folder);
|
||||||
|
childrenMap.set(folder.parentId, children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootFolders = folders.filter((folder) => !folder.parentId || !allFolderIds.has(folder.parentId));
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let currentLevel = rootFolders;
|
||||||
|
|
||||||
|
while (currentLevel.length > 0) {
|
||||||
|
result.push(...currentLevel);
|
||||||
|
|
||||||
|
const nextLevel = [];
|
||||||
|
for (const folder of currentLevel) {
|
||||||
|
if (folder.id) {
|
||||||
|
const children = childrenMap.get(folder.id) || [];
|
||||||
|
nextLevel.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = nextLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.FolderTreeCheckpoint, async (job) => {
|
||||||
|
try {
|
||||||
|
if (job.name === QueueJobs.CreateFolderTreeCheckpoint) {
|
||||||
|
const { envId } = job.data as { envId: string };
|
||||||
|
logger.info("Folder tree checkpoint creation started:", envId, job.id);
|
||||||
|
|
||||||
|
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId);
|
||||||
|
|
||||||
|
const latestCommit = await folderCommitDAL.findLatestEnvCommit(envId);
|
||||||
|
if (!latestCommit) {
|
||||||
|
logger.info(`Latest commit ID not found for envId ${envId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latestCommitId = latestCommit.id;
|
||||||
|
|
||||||
|
if (latestTreeCheckpoint) {
|
||||||
|
const commitsSinceLastCheckpoint = await folderCommitDAL.getEnvNumberOfCommitsSince(
|
||||||
|
envId,
|
||||||
|
latestTreeCheckpoint.folderCommitId
|
||||||
|
);
|
||||||
|
if (commitsSinceLastCheckpoint < Number(appCfg.PIT_TREE_CHECKPOINT_WINDOW)) {
|
||||||
|
logger.info(
|
||||||
|
`Commits since last checkpoint ${commitsSinceLastCheckpoint} is less than ${appCfg.PIT_TREE_CHECKPOINT_WINDOW}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await folderDAL.findByEnvId(envId);
|
||||||
|
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||||
|
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
|
||||||
|
|
||||||
|
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds);
|
||||||
|
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create({
|
||||||
|
folderCommitId: latestCommitId
|
||||||
|
});
|
||||||
|
|
||||||
|
await folderTreeCheckpointResourcesDAL.insertMany(
|
||||||
|
folderCommits.map((folderCommit) => ({
|
||||||
|
folderTreeCheckpointId: folderTreeCheckpoint.id,
|
||||||
|
folderId: folderCommit.folderId,
|
||||||
|
folderCommitId: folderCommit.id
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("Folder tree checkpoint created successfully:", folderTreeCheckpoint.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error creating folder tree checkpoint:");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheduleTreeCheckpoint,
|
||||||
|
schedulePeriodicTreeCheckpoint,
|
||||||
|
cancelScheduledTreeCheckpoint
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { ChangeType, folderCommitServiceFactory, TFolderCommitServiceFactory } from "./folder-commit-service";
|
||||||
|
|
||||||
|
// Mock config
|
||||||
|
vi.mock("@app/lib/config/env", () => ({
|
||||||
|
getConfig: () => ({
|
||||||
|
PIT_CHECKPOINT_WINDOW: 5,
|
||||||
|
PIT_TREE_CHECKPOINT_WINDOW: 10
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock logger
|
||||||
|
vi.mock("@app/lib/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("folderCommitServiceFactory", () => {
|
||||||
|
// Properly type the mock functions
|
||||||
|
type TransactionCallback<T> = (trx: Knex) => Promise<T>;
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
const mockFolderCommitDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestCommit: vi.fn().mockResolvedValue({}),
|
||||||
|
transaction: vi.fn().mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex)),
|
||||||
|
getNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
|
||||||
|
getEnvNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
|
||||||
|
findCommitsToRecreate: vi.fn().mockResolvedValue([]),
|
||||||
|
findMultipleLatestCommits: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestCommitBetween: vi.fn().mockResolvedValue({}),
|
||||||
|
findAllCommitsBetween: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestEnvCommit: vi.fn().mockResolvedValue({}),
|
||||||
|
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCommitChangesDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByCommitId: vi.fn().mockResolvedValue([]),
|
||||||
|
insertMany: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCheckpointDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestByFolderId: vi.fn().mockResolvedValue(null),
|
||||||
|
findNearestCheckpoint: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCheckpointResourcesDAL = {
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
findByCheckpointId: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderTreeCheckpointDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByProjectId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestByProjectId: vi.fn().mockResolvedValue({}),
|
||||||
|
findNearestCheckpoint: vi.fn().mockResolvedValue({}),
|
||||||
|
findLatestByEnvId: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderTreeCheckpointResourcesDAL = {
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
findByTreeCheckpointId: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUserDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIdentityDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderDAL = {
|
||||||
|
findByParentId: vi.fn().mockResolvedValue([]),
|
||||||
|
findByProjectId: vi.fn().mockResolvedValue([]),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findByEnvId: vi.fn().mockResolvedValue([]),
|
||||||
|
findFoldersByRootAndIds: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderVersionDAL = {
|
||||||
|
findLatestFolderVersions: vi.fn().mockResolvedValue({}),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSecretVersionV2BridgeDAL = {
|
||||||
|
findLatestVersionByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSecretV2BridgeDAL = {
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
invalidateSecretCacheByProjectId: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProjectDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCommitQueueService = {
|
||||||
|
scheduleTreeCheckpoint: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
let folderCommitService: TFolderCommitServiceFactory;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
folderCommitService = folderCommitServiceFactory({
|
||||||
|
folderCommitDAL: mockFolderCommitDAL,
|
||||||
|
folderCommitChangesDAL: mockFolderCommitChangesDAL,
|
||||||
|
folderCheckpointDAL: mockFolderCheckpointDAL,
|
||||||
|
folderCheckpointResourcesDAL: mockFolderCheckpointResourcesDAL,
|
||||||
|
folderTreeCheckpointDAL: mockFolderTreeCheckpointDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL: mockFolderTreeCheckpointResourcesDAL,
|
||||||
|
userDAL: mockUserDAL,
|
||||||
|
identityDAL: mockIdentityDAL,
|
||||||
|
folderDAL: mockFolderDAL,
|
||||||
|
folderVersionDAL: mockFolderVersionDAL,
|
||||||
|
secretVersionV2BridgeDAL: mockSecretVersionV2BridgeDAL,
|
||||||
|
projectDAL: mockProjectDAL,
|
||||||
|
secretV2BridgeDAL: mockSecretV2BridgeDAL,
|
||||||
|
folderCommitQueueService: mockFolderCommitQueueService
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCommit", () => {
|
||||||
|
it("should successfully create a commit with user actor", async () => {
|
||||||
|
// Arrange
|
||||||
|
const userData = { id: "user-id", username: "testuser" };
|
||||||
|
const folderData = { id: "folder-id", envId: "env-id" };
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockUserDAL.findById.mockResolvedValue(userData);
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(folderData);
|
||||||
|
mockFolderCommitDAL.create.mockResolvedValue(commitData);
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.USER,
|
||||||
|
metadata: { id: userData.id }
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: folderData.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createCommit(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockUserDAL.findById).toHaveBeenCalledWith(userData.id, undefined);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
actorType: ActorType.USER,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
actorMetadata: expect.objectContaining({ name: userData.username }),
|
||||||
|
message: data.message,
|
||||||
|
folderId: data.folderId,
|
||||||
|
envId: folderData.envId
|
||||||
|
}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: data.changes[0].type,
|
||||||
|
secretVersionId: data.changes[0].secretVersionId
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
|
||||||
|
expect(result).toEqual(commitData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully create a commit with identity actor", async () => {
|
||||||
|
// Arrange
|
||||||
|
const identityData = { id: "identity-id", name: "testidentity" };
|
||||||
|
const folderData = { id: "folder-id", envId: "env-id" };
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockIdentityDAL.findById.mockResolvedValue(identityData);
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(folderData);
|
||||||
|
mockFolderCommitDAL.create.mockResolvedValue(commitData);
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.IDENTITY,
|
||||||
|
metadata: { id: identityData.id }
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: folderData.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
folderVersionId: "folder-version-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createCommit(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockIdentityDAL.findById).toHaveBeenCalledWith(identityData.id, undefined);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
actorType: ActorType.IDENTITY,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
actorMetadata: expect.objectContaining({ name: identityData.name }),
|
||||||
|
message: data.message,
|
||||||
|
folderId: data.folderId,
|
||||||
|
envId: folderData.envId
|
||||||
|
}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: data.changes[0].type,
|
||||||
|
folderVersionId: data.changes[0].folderVersionId
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
|
||||||
|
expect(result).toEqual(commitData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when folder does not exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: "non-existent-folder",
|
||||||
|
changes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.createCommit(data)).rejects.toThrow(NotFoundError);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith("non-existent-folder", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addCommitChange", () => {
|
||||||
|
it("should successfully add a change to an existing commit", async () => {
|
||||||
|
// Arrange
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
const changeData = { id: "change-id", folderCommitId: "commit-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(commitData);
|
||||||
|
mockFolderCommitChangesDAL.create.mockResolvedValue(changeData);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: "add",
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.addCommitChange(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(commitData.id, undefined);
|
||||||
|
expect(mockFolderCommitChangesDAL.create).toHaveBeenCalledWith(data, undefined);
|
||||||
|
expect(result).toEqual(changeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw BadRequestError when neither secretVersionId nor folderVersionId is provided", async () => {
|
||||||
|
// Arrange
|
||||||
|
const data = {
|
||||||
|
folderCommitId: "commit-id",
|
||||||
|
changeType: "add"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(BadRequestError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when commit does not exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
folderCommitId: "non-existent-commit",
|
||||||
|
changeType: "add",
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(NotFoundError);
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith("non-existent-commit", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: reconstructFolderState is an internal function not exposed in the public API
|
||||||
|
// We'll test it indirectly through compareFolderStates
|
||||||
|
|
||||||
|
describe("compareFolderStates", () => {
|
||||||
|
it("should mark all resources as creates when currentCommitId is not provided", async () => {
|
||||||
|
// Arrange
|
||||||
|
const targetCommitId = "target-commit-id";
|
||||||
|
const targetCommit = { id: targetCommitId, commitId: 1, folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(targetCommit);
|
||||||
|
// Mock how compareFolderStates would process the results internally
|
||||||
|
mockFolderCheckpointDAL.findNearestCheckpoint.mockResolvedValue({ id: "checkpoint-id", commitId: "hash-0" });
|
||||||
|
mockFolderCheckpointResourcesDAL.findByCheckpointId.mockResolvedValue([
|
||||||
|
{ secretVersionId: "secret-version-1", referencedSecretId: "secret-1" },
|
||||||
|
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
|
||||||
|
]);
|
||||||
|
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.compareFolderStates({
|
||||||
|
targetCommitId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(targetCommitId, undefined);
|
||||||
|
|
||||||
|
// Verify we get resources marked as create
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
changeType: "create",
|
||||||
|
commitId: targetCommit.commitId
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createFolderCheckpoint", () => {
|
||||||
|
it("should successfully create a checkpoint when force is true", async () => {
|
||||||
|
// Arrange
|
||||||
|
const folderCommitId = "commit-id";
|
||||||
|
const folderId = "folder-id";
|
||||||
|
const checkpointData = { id: "checkpoint-id", folderCommitId };
|
||||||
|
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([{ id: "subfolder-id" }]);
|
||||||
|
mockFolderVersionDAL.findLatestFolderVersions.mockResolvedValue({ "subfolder-id": { id: "folder-version-1" } });
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([{ id: "secret-version-1" }]);
|
||||||
|
mockFolderCheckpointDAL.create.mockResolvedValue(checkpointData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createFolderCheckpoint({
|
||||||
|
folderId,
|
||||||
|
folderCommitId,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
|
||||||
|
expect(mockFolderCheckpointResourcesDAL.insertMany).toHaveBeenCalled();
|
||||||
|
expect(result).toBe(folderCommitId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deepRollbackFolder", () => {
|
||||||
|
it("should throw NotFoundError when commit doesn't exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
const targetCommitId = "non-existent-commit";
|
||||||
|
const envId = "env-id";
|
||||||
|
const actorId = "user-id";
|
||||||
|
const actorType = ActorType.USER;
|
||||||
|
const projectId = "project-id";
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(
|
||||||
|
folderCommitService.deepRollbackFolder(targetCommitId, envId, actorId, actorType, projectId)
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createFolderTreeCheckpoint", () => {
|
||||||
|
it("should create a tree checkpoint when checkpoint window is exceeded", async () => {
|
||||||
|
// Arrange
|
||||||
|
const envId = "env-id";
|
||||||
|
const folderCommitId = "commit-id";
|
||||||
|
const latestCommit = { id: folderCommitId };
|
||||||
|
const latestTreeCheckpoint = { id: "tree-checkpoint-id", folderCommitId: "old-commit-id" };
|
||||||
|
const folders = [
|
||||||
|
{ id: "folder-1", isReserved: false },
|
||||||
|
{ id: "folder-2", isReserved: false },
|
||||||
|
{ id: "folder-3", isReserved: true } // Reserved folders should be filtered out
|
||||||
|
];
|
||||||
|
const folderCommits = [
|
||||||
|
{ folderId: "folder-1", id: "commit-1" },
|
||||||
|
{ folderId: "folder-2", id: "commit-2" }
|
||||||
|
];
|
||||||
|
const treeCheckpoint = { id: "new-tree-checkpoint-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findLatestEnvCommit.mockResolvedValue(latestCommit);
|
||||||
|
mockFolderTreeCheckpointDAL.findLatestByEnvId.mockResolvedValue(latestTreeCheckpoint);
|
||||||
|
mockFolderCommitDAL.getEnvNumberOfCommitsSince.mockResolvedValue(15); // More than PIT_TREE_CHECKPOINT_WINDOW (10)
|
||||||
|
mockFolderDAL.findByEnvId.mockResolvedValue(folders);
|
||||||
|
mockFolderCommitDAL.findMultipleLatestCommits.mockResolvedValue(folderCommits);
|
||||||
|
mockFolderTreeCheckpointDAL.create.mockResolvedValue(treeCheckpoint);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await folderCommitService.createFolderTreeCheckpoint(envId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findLatestEnvCommit).toHaveBeenCalledWith(envId, undefined);
|
||||||
|
expect(mockFolderTreeCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyFolderStateDifferences", () => {
|
||||||
|
it("should process changes correctly", async () => {
|
||||||
|
// Arrange
|
||||||
|
const folderId = "folder-id";
|
||||||
|
const projectId = "project-id";
|
||||||
|
const actorId = "user-id";
|
||||||
|
const actorType = ActorType.USER;
|
||||||
|
|
||||||
|
const differences = [
|
||||||
|
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: 1 },
|
||||||
|
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const secretVersions = {
|
||||||
|
"secret-1": {
|
||||||
|
id: "secret-version-1",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
type: "shared",
|
||||||
|
folderId: "folder-1",
|
||||||
|
secretId: "secret-1",
|
||||||
|
version: 1,
|
||||||
|
key: "SECRET_KEY",
|
||||||
|
encryptedValue: Buffer.from("encrypted"),
|
||||||
|
encryptedComment: Buffer.from("comment"),
|
||||||
|
skipMultilineEncoding: false,
|
||||||
|
userId: "user-1",
|
||||||
|
envId: "env-1",
|
||||||
|
metadata: {}
|
||||||
|
} as TSecretVersionsV2
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderVersions = {
|
||||||
|
"folder-1": {
|
||||||
|
folderId: "folder-1",
|
||||||
|
version: 1,
|
||||||
|
name: "Test Folder",
|
||||||
|
envId: "env-1"
|
||||||
|
} as TSecretFolderVersions
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock folder lookup for the folder being processed
|
||||||
|
mockFolderDAL.findById.mockImplementation((id) => {
|
||||||
|
if (id === folderId) {
|
||||||
|
return Promise.resolve({ id: folderId, envId: "env-1" });
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock latest commit lookup
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockImplementation((id) => {
|
||||||
|
if (id === folderId) {
|
||||||
|
return Promise.resolve({ id: "latest-commit-id", folderId });
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure findByParentId returns an array, not undefined
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// Make sure other required functions return appropriate values
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// These mocks need to return objects with an id field
|
||||||
|
mockSecretVersionV2BridgeDAL.findByIdsWithLatestVersion.mockResolvedValue(secretVersions);
|
||||||
|
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(folderVersions);
|
||||||
|
mockSecretV2BridgeDAL.insertMany.mockResolvedValue([{ id: "new-secret-1" }]);
|
||||||
|
mockSecretVersionV2BridgeDAL.create.mockResolvedValue({ id: "new-secret-version-1" });
|
||||||
|
mockFolderDAL.updateById.mockResolvedValue({ id: "updated-folder-1" });
|
||||||
|
mockFolderVersionDAL.create.mockResolvedValue({ id: "new-folder-version-1" });
|
||||||
|
mockFolderCommitDAL.create.mockResolvedValue({ id: "new-commit-id" });
|
||||||
|
|
||||||
|
// Mock transaction
|
||||||
|
mockFolderCommitDAL.transaction.mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.applyFolderStateDifferences({
|
||||||
|
differences,
|
||||||
|
actorInfo: {
|
||||||
|
actorType,
|
||||||
|
actorId,
|
||||||
|
message: "Applying changes"
|
||||||
|
},
|
||||||
|
folderId,
|
||||||
|
projectId,
|
||||||
|
reconstructNewFolders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalled();
|
||||||
|
expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId);
|
||||||
|
|
||||||
|
// Check that we got the right counts
|
||||||
|
expect(result).toEqual({
|
||||||
|
secretChangesCount: 1,
|
||||||
|
folderChangesCount: 1,
|
||||||
|
totalChanges: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
+15
-95
@@ -1,27 +1,14 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import {
|
import { TableName, TFolderTreeCheckpointResources } from "@app/db/schemas";
|
||||||
TableName,
|
|
||||||
TFolderTreeCheckpointResources,
|
|
||||||
TFolderTreeCheckpoints,
|
|
||||||
TProjectEnvironments,
|
|
||||||
TSecretFolders
|
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TFolderTreeCheckpointResourcesDALFactory = ReturnType<typeof folderTreeCheckpointResourcesDALFactory>;
|
export type TFolderTreeCheckpointResourcesDALFactory = ReturnType<typeof folderTreeCheckpointResourcesDALFactory>;
|
||||||
|
|
||||||
type ResourceWithCheckpointInfo = TFolderTreeCheckpointResources & {
|
type TFolderTreeCheckpointResourcesWithCommitId = TFolderTreeCheckpointResources & {
|
||||||
folderCommitId: string;
|
commitId: number;
|
||||||
};
|
|
||||||
|
|
||||||
type ResourceWithFolderInfo = TFolderTreeCheckpointResources & {
|
|
||||||
name: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
slug: string;
|
|
||||||
envName: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
|
export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||||
@@ -30,95 +17,28 @@ export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
|
|||||||
const findByTreeCheckpointId = async (
|
const findByTreeCheckpointId = async (
|
||||||
folderTreeCheckpointId: string,
|
folderTreeCheckpointId: string,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TFolderTreeCheckpointResources[]> => {
|
): Promise<TFolderTreeCheckpointResourcesWithCommitId[]> => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())<TFolderTreeCheckpointResources>(
|
const docs = await (tx || db.replicaNode())<TFolderTreeCheckpointResources>(
|
||||||
TableName.FolderTreeCheckpointResources
|
TableName.FolderTreeCheckpointResources
|
||||||
)
|
)
|
||||||
.where({ folderTreeCheckpointId })
|
.join(
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources));
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderTreeCheckpointResources}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderTreeCheckpointId }, TableName.FolderTreeCheckpointResources))
|
||||||
|
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
|
||||||
|
.select(db.ref("commitId").withSchema(TableName.FolderCommit).as("commitId"));
|
||||||
return docs;
|
return docs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByTreeCheckpointId" });
|
throw new DatabaseError({ error, name: "FindByTreeCheckpointId" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderId = async (folderId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
|
|
||||||
try {
|
|
||||||
const docs = await (tx || db.replicaNode())<
|
|
||||||
TFolderTreeCheckpointResources & Pick<TFolderTreeCheckpoints, "folderCommitId" | "createdAt">
|
|
||||||
>(TableName.FolderTreeCheckpointResources)
|
|
||||||
.where({ folderId })
|
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
|
|
||||||
.join(
|
|
||||||
TableName.FolderTreeCheckpoint,
|
|
||||||
`${TableName.FolderTreeCheckpointResources}.folderTreeCheckpointId`,
|
|
||||||
`${TableName.FolderTreeCheckpoint}.id`
|
|
||||||
)
|
|
||||||
.select(
|
|
||||||
db.ref("folderCommitId").withSchema(TableName.FolderTreeCheckpoint),
|
|
||||||
db.ref("createdAt").withSchema(TableName.FolderTreeCheckpoint)
|
|
||||||
);
|
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "FindByFolderId" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findByFolderCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderTreeCheckpointResources[]> => {
|
|
||||||
try {
|
|
||||||
const docs = await (tx || db.replicaNode())<
|
|
||||||
TFolderTreeCheckpointResources & Pick<TFolderTreeCheckpoints, "createdAt">
|
|
||||||
>(TableName.FolderTreeCheckpointResources)
|
|
||||||
.where({ folderCommitId })
|
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
|
|
||||||
.join(
|
|
||||||
TableName.FolderTreeCheckpoint,
|
|
||||||
`${TableName.FolderTreeCheckpointResources}.folderTreeCheckpointId`,
|
|
||||||
`${TableName.FolderTreeCheckpoint}.id`
|
|
||||||
)
|
|
||||||
.select(db.ref("createdAt").withSchema(TableName.FolderTreeCheckpoint));
|
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "FindByFolderCommitId" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findFoldersInTreeCheckpoint = async (
|
|
||||||
folderTreeCheckpointId: string,
|
|
||||||
tx?: Knex
|
|
||||||
): Promise<ResourceWithFolderInfo[]> => {
|
|
||||||
try {
|
|
||||||
const docs = await (tx || db.replicaNode())<
|
|
||||||
TFolderTreeCheckpointResources &
|
|
||||||
Pick<TSecretFolders, "name" | "parentId"> &
|
|
||||||
Pick<TProjectEnvironments, "slug"> & { envName: string }
|
|
||||||
>(TableName.FolderTreeCheckpointResources)
|
|
||||||
.where({ folderTreeCheckpointId })
|
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
|
|
||||||
.join(
|
|
||||||
TableName.SecretFolder,
|
|
||||||
`${TableName.FolderTreeCheckpointResources}.folderId`,
|
|
||||||
`${TableName.SecretFolder}.id`
|
|
||||||
)
|
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
|
||||||
.select(
|
|
||||||
db.ref("name").withSchema(TableName.SecretFolder),
|
|
||||||
db.ref("parentId").withSchema(TableName.SecretFolder),
|
|
||||||
db.ref("slug").withSchema(TableName.Environment),
|
|
||||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
|
||||||
);
|
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "FindFoldersInTreeCheckpoint" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...folderTreeCheckpointResourcesOrm,
|
...folderTreeCheckpointResourcesOrm,
|
||||||
findByTreeCheckpointId,
|
findByTreeCheckpointId
|
||||||
findByFolderId,
|
|
||||||
findByFolderCommitId,
|
|
||||||
findFoldersInTreeCheckpoint
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
export type TFolderTreeCheckpointDALFactory = ReturnType<typeof folderTreeCheckpointDALFactory>;
|
export type TFolderTreeCheckpointDALFactory = ReturnType<typeof folderTreeCheckpointDALFactory>;
|
||||||
|
|
||||||
type TreeCheckpointWithCommitInfo = TFolderTreeCheckpoints & {
|
type TreeCheckpointWithCommitInfo = TFolderTreeCheckpoints & {
|
||||||
actorMetadata: unknown;
|
commitId: number;
|
||||||
actorType: string;
|
|
||||||
message?: string | null;
|
|
||||||
commitDate: Date;
|
|
||||||
folderId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
||||||
@@ -30,82 +26,61 @@ export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectId = async (
|
const findNearestCheckpoint = async (
|
||||||
projectId: string,
|
folderCommitId: string,
|
||||||
limit?: number,
|
envId: string,
|
||||||
tx?: Knex
|
|
||||||
): Promise<TreeCheckpointWithCommitInfo[]> => {
|
|
||||||
try {
|
|
||||||
const query = (tx || db.replicaNode())<
|
|
||||||
TFolderTreeCheckpoints &
|
|
||||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "folderId"> & { commitDate: Date }
|
|
||||||
>(TableName.FolderTreeCheckpoint)
|
|
||||||
.join(
|
|
||||||
TableName.FolderCommit,
|
|
||||||
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
|
||||||
`${TableName.FolderCommit}.id`
|
|
||||||
)
|
|
||||||
.join(TableName.SecretFolder, `${TableName.FolderCommit}.folderId`, `${TableName.SecretFolder}.id`)
|
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
|
||||||
.where({ projectId })
|
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
|
||||||
.select(
|
|
||||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
|
||||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
|
||||||
db.ref("message").withSchema(TableName.FolderCommit),
|
|
||||||
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
|
|
||||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
|
||||||
)
|
|
||||||
.orderBy(`${TableName.FolderTreeCheckpoint}.createdAt`, "desc");
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
void query.limit(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const docs = await query;
|
|
||||||
return docs;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "FindByProjectId" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findLatestByProjectId = async (
|
|
||||||
projectId: string,
|
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TreeCheckpointWithCommitInfo | undefined> => {
|
): Promise<TreeCheckpointWithCommitInfo | undefined> => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db.replicaNode())<
|
const targetCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
TFolderTreeCheckpoints &
|
.where({ id: folderCommitId })
|
||||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "folderId"> & { commitDate: Date }
|
.select("id", "commitId", "folderId")
|
||||||
>(TableName.FolderTreeCheckpoint)
|
.first();
|
||||||
.join(
|
|
||||||
|
if (!targetCommit) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
TableName.FolderCommit,
|
TableName.FolderCommit,
|
||||||
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
||||||
`${TableName.FolderCommit}.id`
|
`${TableName.FolderCommit}.id`
|
||||||
)
|
)
|
||||||
.join(TableName.SecretFolder, `${TableName.FolderCommit}.folderId`, `${TableName.SecretFolder}.id`)
|
.where(`${TableName.FolderCommit}.commitId`, "<=", targetCommit.commitId.toString())
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.where(`${TableName.FolderCommit}.envId`, envId)
|
||||||
.where({ projectId })
|
|
||||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||||
.select(
|
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
||||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
.first();
|
||||||
db.ref("message").withSchema(TableName.FolderCommit),
|
|
||||||
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
|
return nearestCheckpoint;
|
||||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestByEnvId = async (envId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
)
|
)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, envId)
|
||||||
.orderBy(`${TableName.FolderTreeCheckpoint}.createdAt`, "desc")
|
.orderBy(`${TableName.FolderTreeCheckpoint}.createdAt`, "desc")
|
||||||
.first();
|
.first();
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindLatestByProjectId" });
|
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...folderTreeCheckpointOrm,
|
...folderTreeCheckpointOrm,
|
||||||
findByCommitId,
|
findByCommitId,
|
||||||
findByProjectId,
|
findNearestCheckpoint,
|
||||||
findLatestByProjectId
|
findLatestByEnvId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -488,6 +488,51 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findFoldersByRootAndIds = async ({ rootId, folderIds }: { rootId: string; folderIds: string[] }, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
// First, get all descendant folders of rootId
|
||||||
|
const descendants = await (tx || db.replicaNode())
|
||||||
|
.withRecursive("descendants", (qb) =>
|
||||||
|
qb
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
|
db.raw("0 as depth"),
|
||||||
|
db.raw(`'/' as path`),
|
||||||
|
db.ref(`${TableName.Environment}.slug`).as("environment")
|
||||||
|
)
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.where(`${TableName.SecretFolder}.id`, rootId)
|
||||||
|
.union((un) => {
|
||||||
|
void un
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
|
db.raw("descendants.depth + 1 as depth"),
|
||||||
|
db.raw(
|
||||||
|
`CONCAT(
|
||||||
|
CASE WHEN descendants.path = '/' THEN '' ELSE descendants.path END,
|
||||||
|
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
|
||||||
|
)`
|
||||||
|
),
|
||||||
|
db.ref("descendants.environment")
|
||||||
|
)
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
|
.join("descendants", `${TableName.SecretFolder}.parentId`, "descendants.id");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
|
||||||
|
.from("descendants")
|
||||||
|
.whereIn(`id`, folderIds)
|
||||||
|
.orderBy("depth")
|
||||||
|
.orderBy(`name`);
|
||||||
|
|
||||||
|
return descendants;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindFoldersByRootAndIds" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByParentId = async (parentId: string, tx?: Knex) => {
|
const findByParentId = async (parentId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
@@ -499,6 +544,17 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findByEnvId = async (envId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
|
.where({ envId })
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder));
|
||||||
|
return folders;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findByEnvId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretFolderOrm,
|
...secretFolderOrm,
|
||||||
update,
|
update,
|
||||||
@@ -511,6 +567,8 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
findByProjectId,
|
findByProjectId,
|
||||||
findByMultiEnv,
|
findByMultiEnv,
|
||||||
findByEnvsDeep,
|
findByEnvsDeep,
|
||||||
findByParentId
|
findByParentId,
|
||||||
|
findByEnvId,
|
||||||
|
findFoldersByRootAndIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user