PIT: general improvements and fixes

This commit is contained in:
carlosmonastyrski
2025-05-25 00:12:31 -03:00
parent 28a3bf0b94
commit b7b90aea33
21 changed files with 1588 additions and 896 deletions
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { Knex } from "knex";
import { TDbClient } from "@app/db";
@@ -14,32 +15,57 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
export type CommitChangeWithCommitInfo = TFolderCommitChanges & {
// Base type with common fields
type BaseCommitChangeInfo = TFolderCommitChanges & {
actorMetadata: unknown;
actorType: string;
message?: string | null;
folderId: string;
folderName?: string;
folderVersion?: string;
secretKey?: string;
secretVersion?: string;
secretId?: string;
folderChangeId?: string;
objectType?: string;
createdAt: Date;
};
// Secret-specific change
export type SecretCommitChange = BaseCommitChangeInfo & {
resourceType: "secret";
secretKey: string;
changeType: string;
secretVersionId?: string | null;
secretVersion: string;
secretId: string;
versions?: {
secretKey?: string;
secretComment?: string;
secretKey: string;
secretComment: string;
skipMultilineEncoding?: boolean | null;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
metadata?: unknown;
tags?: string[] | null;
secretReminderRecipients?: string[] | null;
secretValue?: string;
secretValue: string;
}[];
};
// Folder-specific change
export type FolderCommitChange = BaseCommitChangeInfo & {
resourceType: "folder";
folderName: string;
folderVersion: string;
folderChangeId: string;
versions?: {
name?: string;
}[];
};
// Discriminated union
export type CommitChangeWithCommitInfo = SecretCommitChange | FolderCommitChange;
// Type guards
export const isSecretCommitChange = (change: CommitChangeWithCommitInfo): change is SecretCommitChange =>
change.resourceType === "secret";
export const isFolderCommitChange = (change: CommitChangeWithCommitInfo): change is FolderCommitChange =>
change.resourceType === "folder";
export const folderCommitChangesDALFactory = (db: TDbClient) => {
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
@@ -50,7 +76,6 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
): Promise<CommitChangeWithCommitInfo[]> => {
try {
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderCommitId }, TableName.FolderCommitChanges))
.leftJoin<TFolderCommits>(
TableName.FolderCommit,
@@ -91,57 +116,108 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
db.ref("createdAt").withSchema(TableName.FolderCommit),
db.ref("folderId").withSchema(TableName.FolderCommit)
);
return docs.map((doc) => ({
...doc,
folderVersion: doc.folderVersion?.toString(),
secretVersion: doc.secretVersion?.toString()
}));
return docs.map((doc) => {
// Determine if this is a secret or folder change based on populated fields
if (doc.secretKey && doc.secretVersion && doc.secretId) {
return {
...doc,
resourceType: "secret",
secretKey: doc.secretKey,
secretVersion: doc.secretVersion.toString(),
secretId: doc.secretId
} as SecretCommitChange;
}
return {
...doc,
resourceType: "folder",
folderName: doc.folderName,
folderVersion: doc.folderVersion.toString(),
folderChangeId: doc.folderChangeId
} as FolderCommitChange;
});
} catch (error) {
throw new DatabaseError({ error, name: "FindByCommitId" });
}
};
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<SecretCommitChange[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCommitChanges &
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
>(TableName.FolderCommitChanges)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ secretVersionId }, TableName.FolderCommitChanges))
.select(selectAllTableCols(TableName.FolderCommitChanges))
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.FolderCommitChanges}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.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),
db.ref("folderId").withSchema(TableName.FolderCommit)
db.ref("folderId").withSchema(TableName.FolderCommit),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
db.ref("secretId").withSchema(TableName.SecretVersionV2)
);
return docs
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId)
.map(
(doc): SecretCommitChange => ({
...doc,
resourceType: "secret",
secretKey: doc.secretKey,
secretVersion: doc.secretVersion.toString(),
secretId: doc.secretId
})
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
}
};
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<FolderCommitChange[]> => {
try {
const docs = await (tx || db.replicaNode())<
TFolderCommitChanges &
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
>(TableName.FolderCommitChanges)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ folderVersionId }, TableName.FolderCommitChanges))
.select(selectAllTableCols(TableName.FolderCommitChanges))
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.FolderCommitChanges}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.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),
db.ref("folderId").withSchema(TableName.FolderCommit)
db.ref("folderId").withSchema(TableName.FolderCommit),
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion")
);
return docs
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId)
.map(
(doc): FolderCommitChange => ({
...doc,
resourceType: "folder",
folderName: doc.folderName,
folderVersion: doc.folderVersion!.toString(),
folderChangeId: doc.folderChangeId
})
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
}
@@ -474,6 +474,24 @@ export const folderCommitDALFactory = (db: TDbClient) => {
}
};
const findCommitBefore = async (
folderId: string,
commitId: bigint,
tx?: Knex
): Promise<TFolderCommits | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
.where({ folderId })
.where("commitId", "<", commitId.toString())
.select(selectAllTableCols(TableName.FolderCommit))
.orderBy("commitId", "desc")
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindCommitBefore" });
}
};
return {
...restOfOrm,
findByFolderId,
@@ -489,6 +507,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
findAllFolderCommitsAfter,
findPreviousCommitTo,
findById,
findByFolderIdPaginated
findByFolderIdPaginated,
findCommitBefore
};
};
@@ -0,0 +1,142 @@
import { z } from "zod";
// Base schema shared by both secret and folder changes
const baseChangeSchema = z.object({
id: z.string(),
folderCommitId: z.string(),
changeType: z.string(),
isUpdate: z.boolean().optional(),
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string(),
message: z.string().nullable().optional(),
folderId: z.string()
});
// Secret-specific versions schema
const secretVersionSchema = z.object({
secretKey: z.string(),
secretComment: z.string(),
skipMultilineEncoding: z.boolean().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
tags: z.array(z.string()).nullable().optional(),
metadata: z.unknown().nullable().optional(),
secretReminderNote: z.string().nullable().optional(),
secretValue: z.string()
});
// Folder-specific versions schema
const folderVersionSchema = z.object({
name: z.string().optional()
});
// Secret commit change schema
const secretCommitChangeSchema = baseChangeSchema.extend({
resourceType: z.literal("secret"),
secretVersionId: z.string().optional().nullable(),
secretKey: z.string(),
secretVersion: z.union([z.string(), z.number()]),
secretId: z.string(),
versions: z.array(secretVersionSchema).optional()
});
// Folder commit change schema
const folderCommitChangeSchema = baseChangeSchema.extend({
resourceType: z.literal("folder"),
folderVersionId: z.string().optional().nullable(),
folderName: z.string(),
folderChangeId: z.string(),
folderVersion: z.union([z.string(), z.number()]),
versions: z.array(folderVersionSchema).optional()
});
// Discriminated union for commit changes
export const commitChangeSchema = z.discriminatedUnion("resourceType", [
secretCommitChangeSchema,
folderCommitChangeSchema
]);
// Commit schema
const commitSchema = z.object({
id: z.string(),
commitId: z.string(),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string(),
message: z.string().nullable().optional(),
folderId: z.string(),
envId: z.string(),
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
changes: z.array(commitChangeSchema).optional()
});
// Response schema
export const commitChangesResponseSchema = z.object({
changes: commitSchema
});
// Base resource change schema for comparison results
const baseResourceChangeSchema = z.object({
id: z.string(),
versionId: z.string(),
oldVersionId: z.string().optional(),
changeType: z.enum(["add", "delete", "update", "create"]),
commitId: z.union([z.string(), z.bigint()]),
createdAt: z.union([z.string(), z.date()]).optional(),
parentId: z.string().optional(),
isUpdate: z.boolean().optional(),
fromVersion: z.union([z.string(), z.number()]).optional()
});
// Secret resource change schema
const secretResourceChangeSchema = baseResourceChangeSchema.extend({
type: z.literal("secret"),
secretKey: z.string().optional(),
secretVersion: z.union([z.string(), z.number()]).optional(),
secretId: z.string().optional(),
versions: z
.array(
z.object({
secretKey: z.string().optional(),
secretComment: z.string().optional(),
skipMultilineEncoding: z.boolean().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
tags: z.array(z.string()).nullable().optional(),
metadata: z.unknown().nullable().optional(),
secretReminderNote: z.string().nullable().optional(),
secretValue: z.string().optional()
})
)
.optional()
});
// Folder resource change schema
const folderResourceChangeSchema = baseResourceChangeSchema.extend({
type: z.literal("folder"),
folderName: z.string().optional(),
folderVersion: z.union([z.string(), z.number()]).optional(),
versions: z.array(folderVersionSchema).optional()
});
// Discriminated union for resource changes
export const resourceChangeSchema = z.discriminatedUnion("type", [
secretResourceChangeSchema,
folderResourceChangeSchema
]);
@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/return-await */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Knex } from "knex";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -9,6 +12,7 @@ import {
ChangeType,
CommitType,
folderCommitServiceFactory,
ResourceType,
TFolderCommitServiceFactory
} from "./folder-commit-service";
@@ -518,8 +522,20 @@ describe("folderCommitServiceFactory", () => {
const actorType = ActorType.USER;
const differences = [
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: BigInt(1) },
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) }
{
type: ResourceType.SECRET,
id: "secret-1",
versionId: "v1",
changeType: ChangeType.CREATE,
commitId: BigInt(1)
},
{
type: ResourceType.FOLDER,
id: "folder-1",
versionId: "v2",
changeType: ChangeType.UPDATE,
commitId: BigInt(1)
}
];
const secretVersions = {
@@ -38,7 +38,7 @@ export enum CommitType {
DELETE = "delete"
}
enum ResourceType {
export enum ResourceType {
SECRET = "secret",
FOLDER = "folder"
}
@@ -68,8 +68,7 @@ type TCommitChangeDTO = {
folderVersionId?: string;
};
export type ResourceChange = {
type: string;
type BaseChange = {
id: string;
versionId: string;
oldVersionId?: string;
@@ -78,12 +77,14 @@ export type ResourceChange = {
createdAt?: Date;
parentId?: string;
isUpdate?: boolean;
fromVersion?: string;
};
type SecretChange = {
type: ResourceType.SECRET;
secretKey?: string;
secretVersion?: string;
secretId?: string;
folderName?: string;
folderVersion?: string;
fromVersion?: string;
versions?: {
secretKey?: string;
secretComment?: string;
@@ -94,10 +95,20 @@ export type ResourceChange = {
tags?: string[] | null;
secretReminderRecipients?: string[] | null;
secretValue?: string;
name?: string;
}[];
};
type FolderChange = {
type: ResourceType.FOLDER;
folderName?: string;
folderVersion?: string;
versions?: {
name: string;
}[];
};
export type ResourceChange = BaseChange & (SecretChange | FolderChange);
type ActorInfo = {
actorType: string;
actorId?: string;
@@ -150,7 +161,7 @@ export const folderCommitServiceFactory = ({
}: TFolderCommitServiceFactoryDep) => {
const appCfg = getConfig();
const checkProjectPermission = async ({
const checkProjectCommitReadPermission = async ({
actor,
actorId,
projectId,
@@ -377,7 +388,7 @@ export const folderCommitServiceFactory = ({
targetCommitId: string;
defaultOperation?: "create" | "update" | "delete";
tx?: Knex;
}) => {
}): Promise<ResourceChange[]> => {
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
if (!targetCommit) {
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
@@ -387,17 +398,34 @@ export const folderCommitServiceFactory = ({
if (!currentCommitId) {
const targetState = await reconstructFolderState(targetCommitId, tx);
return targetState.map((resource) => ({
type: resource.type,
id: resource.id,
versionId: resource.versionId,
changeType: defaultOperation,
commitId: targetCommit.commitId,
secretKey: resource.secretKey,
secretVersion: resource.secretVersion,
folderName: resource.folderName,
folderVersion: resource.folderVersion
})) as ResourceChange[];
return targetState
.map((resource): ResourceChange | null => {
if (resource.type === ResourceType.SECRET) {
return {
type: ResourceType.SECRET,
id: resource.id,
versionId: resource.versionId,
changeType: defaultOperation as ChangeType,
commitId: targetCommit.commitId,
secretKey: resource.secretKey,
secretVersion: resource.secretVersion,
secretId: resource.id
};
}
if (resource.type === ResourceType.FOLDER) {
return {
type: ResourceType.FOLDER,
id: resource.id,
versionId: resource.versionId,
changeType: defaultOperation as ChangeType,
commitId: targetCommit.commitId,
folderName: resource.folderName,
folderVersion: resource.folderVersion
};
}
return null;
})
.filter((change): change is ResourceChange => !!change);
}
// Original logic for when currentCommitId is provided
@@ -452,49 +480,88 @@ export const folderCommitServiceFactory = ({
const targetResource = targetMap[key];
if (!targetResource) {
differences.push({
type: currentResource.type,
id: currentResource.id,
versionId: currentResource.versionId,
changeType: ChangeType.DELETE,
commitId: targetCommit.commitId,
secretKey: currentResource.secretKey,
secretVersion: currentResource.secretVersion,
folderName: currentResource.folderName,
folderVersion: currentResource.folderVersion,
fromVersion: currentResource.versionId
});
// Resource was deleted
if (currentResource.type === ResourceType.SECRET) {
differences.push({
type: ResourceType.SECRET,
id: currentResource.id,
versionId: currentResource.versionId,
changeType: ChangeType.DELETE,
commitId: targetCommit.commitId,
secretKey: currentResource.secretKey,
secretVersion: currentResource.secretVersion,
secretId: currentResource.id,
fromVersion: currentResource.versionId
});
} else if (currentResource.type === ResourceType.FOLDER) {
differences.push({
type: ResourceType.FOLDER,
id: currentResource.id,
versionId: currentResource.versionId,
changeType: ChangeType.DELETE,
commitId: targetCommit.commitId,
folderName: currentResource.folderName,
folderVersion: currentResource.folderVersion,
fromVersion: currentResource.versionId
});
}
} else if (currentResource.versionId !== targetResource.versionId) {
differences.push({
type: targetResource.type,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.UPDATE,
commitId: targetCommit.commitId,
secretKey: targetResource.secretKey,
secretVersion: targetResource.secretVersion,
folderName: targetResource.folderName,
folderVersion: targetResource.folderVersion,
fromVersion: currentResource.folderVersion || currentResource.secretVersion
});
// Resource was updated
if (targetResource.type === ResourceType.SECRET) {
differences.push({
type: ResourceType.SECRET,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.UPDATE,
commitId: targetCommit.commitId,
secretKey: targetResource.secretKey,
secretVersion: targetResource.secretVersion,
secretId: targetResource.id,
fromVersion: currentResource.secretVersion
});
} else if (targetResource.type === ResourceType.FOLDER) {
differences.push({
type: ResourceType.FOLDER,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.UPDATE,
commitId: targetCommit.commitId,
folderName: targetResource.folderName,
folderVersion: targetResource.folderVersion,
fromVersion: currentResource.folderVersion
});
}
}
});
// Find new resources
Object.keys(targetMap).forEach((key) => {
if (!currentMap[key]) {
const targetResource = targetMap[key];
differences.push({
type: targetResource.type,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.CREATE,
commitId: targetCommit.commitId,
createdAt: targetCommit.createdAt,
secretKey: targetResource.secretKey,
secretVersion: targetResource.secretVersion,
folderName: targetResource.folderName,
folderVersion: targetResource.folderVersion
});
if (targetResource.type === ResourceType.SECRET) {
differences.push({
type: ResourceType.SECRET,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.CREATE,
commitId: targetCommit.commitId,
createdAt: targetCommit.createdAt,
secretKey: targetResource.secretKey,
secretVersion: targetResource.secretVersion,
secretId: targetResource.id
});
} else if (targetResource.type === ResourceType.FOLDER) {
differences.push({
type: ResourceType.FOLDER,
id: targetResource.id,
versionId: targetResource.versionId,
changeType: ChangeType.CREATE,
commitId: targetCommit.commitId,
createdAt: targetCommit.createdAt,
folderName: targetResource.folderName,
folderVersion: targetResource.folderVersion
});
}
}
});
@@ -640,13 +707,18 @@ export const folderCommitServiceFactory = ({
) => {
const commitChanges = [];
// Filter only secret changes using discriminated union
const secretChanges = changes.filter(
(change): change is ResourceChange & SecretChange => change.type === ResourceType.SECRET
);
// Collect all secretIds for batch lookup
const secretIds = changes.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
const secretIds = secretChanges.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
// Fetch all latest versions in one call
const latestVersionsMap = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds, tx);
for (const change of changes) {
for (const change of secretChanges) {
const secretVersion = secretVersions[change.id];
// eslint-disable-next-line no-continue
if (!secretVersion) continue;
@@ -800,7 +872,12 @@ export const folderCommitServiceFactory = ({
) => {
const commitChanges = [];
for (const change of changes) {
// Filter only folder changes using discriminated union
const folderChanges = changes.filter(
(change): change is ResourceChange & FolderChange => change.type === ResourceType.FOLDER
);
for (const change of folderChanges) {
const folderVersion = folderVersions[change.id];
switch (change.changeType) {
@@ -913,9 +990,14 @@ export const folderCommitServiceFactory = ({
return commitChanges;
};
// Group differences by type for more efficient processing
const secretChanges = differences.filter((diff) => diff.type === ResourceType.SECRET);
const folderChanges = differences.filter((diff) => diff.type === ResourceType.FOLDER);
// Group differences by type for more efficient processing using discriminated unions
const secretChanges = differences.filter(
(diff): diff is ResourceChange & SecretChange => diff.type === ResourceType.SECRET
);
const folderChanges = differences.filter(
(diff): diff is ResourceChange & FolderChange => diff.type === ResourceType.FOLDER
);
// Batch fetch necessary data
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
@@ -933,8 +1015,8 @@ export const folderCommitServiceFactory = ({
// Process changes in parallel
const [secretCommitChanges, folderCommitChanges] = await Promise.all([
processSecretChanges(secretChanges, secretVersions, actorInfo, folderId, tx),
processFolderChanges(folderChanges, folderVersions)
processSecretChanges(differences, secretVersions, actorInfo, folderId, tx),
processFolderChanges(differences, folderVersions)
]);
// Combine all changes
@@ -988,8 +1070,31 @@ export const folderCommitServiceFactory = ({
/**
* Retrieve a commit by ID
*/
const getCommitById = async (id: string, tx?: Knex) => {
return folderCommitDAL.findById(id, tx);
const getCommitById = async ({
commitId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
tx
}: {
commitId: string;
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
tx?: Knex;
}) => {
await checkProjectCommitReadPermission({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId
});
return folderCommitDAL.findById(commitId, tx);
};
/**
@@ -1024,7 +1129,7 @@ export const folderCommitServiceFactory = ({
search?: string;
sort: "asc" | "desc";
}) => {
await checkProjectPermission({
await checkProjectCommitReadPermission({
actor,
actorId,
actorAuthMethod,
@@ -1063,7 +1168,7 @@ export const folderCommitServiceFactory = ({
environment: string;
path: string;
}) => {
await checkProjectPermission({
await checkProjectCommitReadPermission({
actor,
actorId,
actorAuthMethod,
@@ -1099,7 +1204,7 @@ export const folderCommitServiceFactory = ({
projectId: string;
commitId: string;
}) => {
await checkProjectPermission({
await checkProjectCommitReadPermission({
actor,
actorId,
actorAuthMethod,
@@ -1382,7 +1487,8 @@ export const folderCommitServiceFactory = ({
envId: string,
actorId: string,
actorType: ActorType,
projectId: string
projectId: string,
message?: string
) => {
await folderCommitDAL.transaction(async (tx) => {
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
@@ -1480,7 +1586,7 @@ export const folderCommitServiceFactory = ({
actorInfo: {
actorType,
actorId,
message: "Deep rollback"
message: message || "Deep rollback"
},
folderId: folder.id,
projectId,
@@ -1508,7 +1614,7 @@ export const folderCommitServiceFactory = ({
actorOrgId: string;
projectId: string;
}) => {
await checkProjectPermission({
await checkProjectCommitReadPermission({
actor,
actorId,
actorAuthMethod,
@@ -1555,7 +1661,7 @@ export const folderCommitServiceFactory = ({
ProjectPermissionSub.Commits
);
// Check permissions first
await checkProjectPermission({
await checkProjectCommitReadPermission({
actor,
actorId,
projectId,
@@ -1569,32 +1675,12 @@ export const folderCommitServiceFactory = ({
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
}
// Get all commits for this folder
const allCommits = await folderCommitDAL.findByFolderId(commitToRevert.folderId);
if (!allCommits || allCommits.length === 0) {
throw new NotFoundError({ message: `No commits found for folder ${commitToRevert.folderId}` });
}
const previousCommit = await folderCommitDAL.findCommitBefore(commitToRevert.folderId, commitToRevert.commitId);
// Sort commits by commitId (which appears to be numeric)
const sortedCommits = allCommits.sort((a, b) => {
if (a.commitId < b.commitId) return -1;
if (a.commitId > b.commitId) return 1;
return 0;
});
// Find the index of the commit to revert
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
if (commitIndex === -1) {
throw new NotFoundError({ message: `Commit ${commitId} not found in the commit history` });
}
// If it's the first commit, we can't revert it (nothing before it)
if (commitIndex === 0) {
if (!previousCommit) {
throw new BadRequestError({ message: "Cannot revert the first commit" });
}
// Get the commit just before the one we want to revert
const previousCommit = sortedCommits[commitIndex - 1];
// Calculate the changes needed to go from current commit back to the previous one
const inverseChanges = await compareFolderStates({
currentCommitId: commitToRevert.id,
@@ -1,74 +0,0 @@
import { z } from "zod";
const baseChangeSchema = z.object({
id: z.string(),
folderCommitId: z.string(),
changeType: z.string(),
isUpdate: z.boolean().optional(),
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string().optional(),
message: z.string().optional().nullable(),
folderId: z.string().optional()
});
const commitChangeSchema = baseChangeSchema.extend({
secretVersionId: z.string().optional().nullable(),
folderVersionId: z.string().optional().nullable(),
folderName: z.string().optional().nullable(),
folderChangeId: z.string().optional().nullable(),
secretKey: z.string().optional().nullable(),
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
secretId: z.string().optional().nullable(),
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
versions: z
.array(
z.object({
secretKey: z.string().optional().nullable(),
secretComment: z.string().optional().nullable(),
skipMultilineEncoding: z.boolean().optional().nullable(),
secretReminderRepeatDays: z.number().optional().nullable(),
secretReminderNote: z.string().optional().nullable(),
metadata: z.unknown().optional().nullable(),
tags: z.array(z.string()).optional().nullable(),
secretReminderRecipients: z.array(z.any()).optional().nullable(),
secretValue: z.string().optional().nullable(),
name: z.string().optional().nullable()
})
)
.optional()
});
const commitSchema = z.object({
id: z.string(),
commitId: z.string(),
actorMetadata: z
.union([
z.object({
id: z.string().optional(),
name: z.string().optional()
}),
z.unknown()
])
.optional(),
actorType: z.string(),
message: z.string().nullable().optional(),
folderId: z.string(),
envId: z.string(),
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
changes: z.array(commitChangeSchema).optional()
});
export const commitChangesResponseSchema = z.object({
changes: commitSchema
});
@@ -10,7 +10,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
@@ -783,6 +783,27 @@ export const secretFolderServiceFactory = ({
return versions;
};
const getFolderVersions = async (
change: {
folderVersion?: string;
isUpdate?: boolean;
changeType?: string;
},
fromVersion: string,
folderId: string
) => {
const currentVersion = change.folderVersion || "1";
// eslint-disable-next-line no-await-in-loop
const versions = await getFolderVersionsByIds({
folderId,
folderVersions:
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
});
return versions.map((v) => ({
name: v.name
}));
};
return {
createFolder,
updateFolder,
@@ -794,6 +815,7 @@ export const secretFolderServiceFactory = ({
getFoldersMultiEnv,
getFoldersDeepByEnvs,
getProjectEnvironmentsFolders,
getFolderVersionsByIds
getFolderVersionsByIds,
getFolderVersions
};
};
@@ -128,6 +128,7 @@ export const fnSecretBulkInsert = async ({
userActorId,
identityActorId,
actorType,
metadata: el.metadata ? JSON.stringify(el.metadata) : null,
secretId: newSecretGroupedByKeyName[el.key][0].id
})),
tx
@@ -273,7 +274,7 @@ export const fnSecretBulkUpdate = async ({
userId,
encryptedComment,
version,
metadata,
metadata: metadata ? JSON.stringify(metadata) : null,
reminderNote,
encryptedValue,
reminderRepeatDays,
@@ -16,6 +16,7 @@ import {
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionCommitsActions,
ProjectPermissionSecretActions,
ProjectPermissionSet,
@@ -528,6 +529,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
key: inputSecret.newSecretName || secretName,
tags: inputSecret.tagIds,
metadata: JSON.stringify(secretMetadata),
secretMetadata,
...encryptedValue
}
@@ -2173,7 +2175,13 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
const canRead =
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
@@ -2889,7 +2897,13 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
const canRead =
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
@@ -199,13 +199,13 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2}.id`,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.where((qb) => {
+49 -2
View File
@@ -44,7 +44,8 @@ import {
TGetSecretsRawByFolderMappingsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { ChangeType } from "../folder-commit/folder-commit-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -3303,6 +3304,51 @@ export const secretServiceFactory = ({
return secrets;
};
const getChangeVersions = async (
change: {
secretVersion: string;
secretId?: string;
id?: string;
isUpdate?: boolean;
changeType?: string;
},
previousVersion: string,
actorId: string,
actor: ActorType,
actorOrgId: string,
actorAuthMethod: ActorAuthMethod,
folderId: string
) => {
const currentVersion = change.secretVersion;
const secretId = change.secretId ? change.secretId : change.id;
if (!secretId) {
return;
}
const versions = await getSecretVersionsV2ByIds({
actorId,
actor,
actorOrgId,
actorAuthMethod,
secretId,
// if it's update add also the previous secretversionid
secretVersions:
change.isUpdate || change.changeType === ChangeType.UPDATE
? [currentVersion, previousVersion]
: [currentVersion],
folderId
});
return versions?.map((v) => ({
secretKey: v.secretKey,
secretComment: v.secretComment,
skipMultilineEncoding: v.skipMultilineEncoding,
secretReminderRepeatDays: v.secretReminderRepeatDays,
tags: v.tags?.map((tag) => tag.slug),
metadata: v.metadata,
secretReminderNote: v.secretReminderNote,
secretValue: v.secretValue
}));
};
return {
attachTags,
detachTags,
@@ -3334,6 +3380,7 @@ export const secretServiceFactory = ({
getSecretAccessList,
getSecretByIdRaw,
getAccessibleSecrets,
getSecretVersionsV2ByIds
getSecretVersionsV2ByIds,
getChangeVersions
};
};