mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
Merge pull request #3555 from Infisical/feat/point-in-time-revamp-2710
Feat/point in time revamp 2710
This commit is contained in:
Vendored
+2
@@ -57,6 +57,7 @@ import { TCertificateTemplateServiceFactory } from "@app/services/certificate-te
|
||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@@ -252,6 +253,7 @@ declare module "fastify" {
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
folderCommit: TFolderCommitServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+48
@@ -74,6 +74,24 @@ import {
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TFolderCheckpointResources,
|
||||
TFolderCheckpointResourcesInsert,
|
||||
TFolderCheckpointResourcesUpdate,
|
||||
TFolderCheckpoints,
|
||||
TFolderCheckpointsInsert,
|
||||
TFolderCheckpointsUpdate,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommitChangesInsert,
|
||||
TFolderCommitChangesUpdate,
|
||||
TFolderCommits,
|
||||
TFolderCommitsInsert,
|
||||
TFolderCommitsUpdate,
|
||||
TFolderTreeCheckpointResources,
|
||||
TFolderTreeCheckpointResourcesInsert,
|
||||
TFolderTreeCheckpointResourcesUpdate,
|
||||
TFolderTreeCheckpoints,
|
||||
TFolderTreeCheckpointsInsert,
|
||||
TFolderTreeCheckpointsUpdate,
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
@@ -1048,5 +1066,35 @@ declare module "knex/types/tables" {
|
||||
TGithubOrgSyncConfigsInsert,
|
||||
TGithubOrgSyncConfigsUpdate
|
||||
>;
|
||||
[TableName.FolderCommit]: KnexOriginal.CompositeTableType<
|
||||
TFolderCommits,
|
||||
TFolderCommitsInsert,
|
||||
TFolderCommitsUpdate
|
||||
>;
|
||||
[TableName.FolderCommitChanges]: KnexOriginal.CompositeTableType<
|
||||
TFolderCommitChanges,
|
||||
TFolderCommitChangesInsert,
|
||||
TFolderCommitChangesUpdate
|
||||
>;
|
||||
[TableName.FolderCheckpoint]: KnexOriginal.CompositeTableType<
|
||||
TFolderCheckpoints,
|
||||
TFolderCheckpointsInsert,
|
||||
TFolderCheckpointsUpdate
|
||||
>;
|
||||
[TableName.FolderCheckpointResources]: KnexOriginal.CompositeTableType<
|
||||
TFolderCheckpointResources,
|
||||
TFolderCheckpointResourcesInsert,
|
||||
TFolderCheckpointResourcesUpdate
|
||||
>;
|
||||
[TableName.FolderTreeCheckpoint]: KnexOriginal.CompositeTableType<
|
||||
TFolderTreeCheckpoints,
|
||||
TFolderTreeCheckpointsInsert,
|
||||
TFolderTreeCheckpointsUpdate
|
||||
>;
|
||||
[TableName.FolderTreeCheckpointResources]: KnexOriginal.CompositeTableType<
|
||||
TFolderTreeCheckpointResources,
|
||||
TFolderTreeCheckpointResourcesInsert,
|
||||
TFolderTreeCheckpointResourcesUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||
if (!hasFolderCommitTable) {
|
||||
await knex.schema.createTable(TableName.FolderCommit, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.bigIncrements("commitId");
|
||||
t.jsonb("actorMetadata").notNullable();
|
||||
t.string("actorType").notNullable();
|
||||
t.string("message");
|
||||
t.uuid("folderId").notNullable();
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderId");
|
||||
t.index("envId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
|
||||
if (!hasFolderCommitChangesTable) {
|
||||
await knex.schema.createTable(TableName.FolderCommitChanges, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.uuid("folderCommitId").notNullable();
|
||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||
t.string("changeType").notNullable();
|
||||
t.boolean("isUpdate").notNullable().defaultTo(false);
|
||||
t.uuid("secretVersionId");
|
||||
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
|
||||
t.uuid("folderVersionId");
|
||||
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderCommitId");
|
||||
t.index("secretVersionId");
|
||||
t.index("folderVersionId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
|
||||
if (!hasFolderCheckpointTable) {
|
||||
await knex.schema.createTable(TableName.FolderCheckpoint, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.uuid("folderCommitId").notNullable();
|
||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderCommitId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
|
||||
if (!hasFolderCheckpointResourcesTable) {
|
||||
await knex.schema.createTable(TableName.FolderCheckpointResources, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.uuid("folderCheckpointId").notNullable();
|
||||
t.foreign("folderCheckpointId").references("id").inTable(TableName.FolderCheckpoint).onDelete("CASCADE");
|
||||
t.uuid("secretVersionId");
|
||||
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
|
||||
t.uuid("folderVersionId");
|
||||
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderCheckpointId");
|
||||
t.index("secretVersionId");
|
||||
t.index("folderVersionId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
|
||||
if (!hasFolderTreeCheckpointTable) {
|
||||
await knex.schema.createTable(TableName.FolderTreeCheckpoint, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.uuid("folderCommitId").notNullable();
|
||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderCommitId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
|
||||
if (!hasFolderTreeCheckpointResourcesTable) {
|
||||
await knex.schema.createTable(TableName.FolderTreeCheckpointResources, (t) => {
|
||||
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||
t.uuid("folderTreeCheckpointId").notNullable();
|
||||
t.foreign("folderTreeCheckpointId").references("id").inTable(TableName.FolderTreeCheckpoint).onDelete("CASCADE");
|
||||
t.uuid("folderId").notNullable();
|
||||
t.uuid("folderCommitId").notNullable();
|
||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.index("folderTreeCheckpointId");
|
||||
t.index("folderId");
|
||||
t.index("folderCommitId");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.FolderCommit);
|
||||
await createOnUpdateTrigger(knex, TableName.FolderCommitChanges);
|
||||
await createOnUpdateTrigger(knex, TableName.FolderCheckpoint);
|
||||
await createOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
|
||||
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
|
||||
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
|
||||
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
|
||||
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
|
||||
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
|
||||
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
|
||||
|
||||
if (hasFolderTreeCheckpointResourcesTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpointResources);
|
||||
}
|
||||
|
||||
if (hasFolderCheckpointResourcesTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderCheckpointResources);
|
||||
}
|
||||
|
||||
if (hasFolderTreeCheckpointTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpoint);
|
||||
}
|
||||
|
||||
if (hasFolderCheckpointTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderCheckpoint);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderCheckpoint);
|
||||
}
|
||||
|
||||
if (hasFolderCommitChangesTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderCommitChanges);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderCommitChanges);
|
||||
}
|
||||
|
||||
if (hasFolderCommitTable) {
|
||||
await dropOnUpdateTrigger(knex, TableName.FolderCommit);
|
||||
await knex.schema.dropTableIfExists(TableName.FolderCommit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
import { getMigrationPITServices } from "./utils/services";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||
if (hasFolderCommitTable) {
|
||||
const keyStore = inMemoryKeyStore();
|
||||
const { folderCommitService } = await getMigrationPITServices({ db: knex, keyStore });
|
||||
const projects = await knex(TableName.Project).where({ version: 3, type: ProjectType.SecretManager }).select("id");
|
||||
await knex.transaction(async (tx) => {
|
||||
for (const project of projects) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await folderCommitService.initializeProject(project.id, tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||
if (hasFolderCommitTable) {
|
||||
// delete all existing entries
|
||||
await knex(TableName.FolderCommit).del();
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,25 @@ import { Knex } from "knex";
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
||||
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
||||
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
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 { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { userDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TMigrationEnvConfig } from "./env-config";
|
||||
|
||||
@@ -50,3 +63,37 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
|
||||
|
||||
return { kmsService };
|
||||
};
|
||||
|
||||
export const getMigrationPITServices = async ({ db, keyStore }: { db: Knex; keyStore: TKeyStoreFactory }) => {
|
||||
const projectDAL = projectDALFactory(db);
|
||||
const folderCommitDAL = folderCommitDALFactory(db);
|
||||
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
||||
const folderCheckpointDAL = folderCheckpointDALFactory(db);
|
||||
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
||||
const userDAL = userDALFactory(db);
|
||||
const identityDAL = identityDALFactory(db);
|
||||
const folderDAL = secretFolderDALFactory(db);
|
||||
const folderVersionDAL = secretFolderVersionDALFactory(db);
|
||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||
|
||||
const folderCommitService = folderCommitServiceFactory({
|
||||
folderCommitDAL,
|
||||
folderCommitChangesDAL,
|
||||
folderCheckpointDAL,
|
||||
folderTreeCheckpointDAL,
|
||||
userDAL,
|
||||
identityDAL,
|
||||
folderDAL,
|
||||
folderVersionDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
projectDAL,
|
||||
folderCheckpointResourcesDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL
|
||||
});
|
||||
|
||||
return { folderCommitService };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderCheckpointResourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderCheckpointId: z.string().uuid(),
|
||||
secretVersionId: z.string().uuid().nullable().optional(),
|
||||
folderVersionId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderCheckpointResources = z.infer<typeof FolderCheckpointResourcesSchema>;
|
||||
export type TFolderCheckpointResourcesInsert = Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>;
|
||||
export type TFolderCheckpointResourcesUpdate = Partial<
|
||||
Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderCheckpointsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderCommitId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderCheckpoints = z.infer<typeof FolderCheckpointsSchema>;
|
||||
export type TFolderCheckpointsInsert = Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>;
|
||||
export type TFolderCheckpointsUpdate = Partial<Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderCommitChangesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderCommitId: z.string().uuid(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().default(false),
|
||||
secretVersionId: z.string().uuid().nullable().optional(),
|
||||
folderVersionId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderCommitChanges = z.infer<typeof FolderCommitChangesSchema>;
|
||||
export type TFolderCommitChangesInsert = Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>;
|
||||
export type TFolderCommitChangesUpdate = Partial<Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderCommitsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
commitId: z.coerce.number(),
|
||||
actorMetadata: z.unknown(),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable().optional(),
|
||||
folderId: z.string().uuid(),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderCommits = z.infer<typeof FolderCommitsSchema>;
|
||||
export type TFolderCommitsInsert = Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>;
|
||||
export type TFolderCommitsUpdate = Partial<Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderTreeCheckpointResourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderTreeCheckpointId: z.string().uuid(),
|
||||
folderId: z.string().uuid(),
|
||||
folderCommitId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderTreeCheckpointResources = z.infer<typeof FolderTreeCheckpointResourcesSchema>;
|
||||
export type TFolderTreeCheckpointResourcesInsert = Omit<
|
||||
z.input<typeof FolderTreeCheckpointResourcesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TFolderTreeCheckpointResourcesUpdate = Partial<
|
||||
Omit<z.input<typeof FolderTreeCheckpointResourcesSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const FolderTreeCheckpointsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderCommitId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TFolderTreeCheckpoints = z.infer<typeof FolderTreeCheckpointsSchema>;
|
||||
export type TFolderTreeCheckpointsInsert = Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>;
|
||||
export type TFolderTreeCheckpointsUpdate = Partial<Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>>;
|
||||
@@ -22,6 +22,12 @@ export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-group-org-role-mappings";
|
||||
export * from "./external-kms";
|
||||
export * from "./folder-checkpoint-resources";
|
||||
export * from "./folder-checkpoints";
|
||||
export * from "./folder-commit-changes";
|
||||
export * from "./folder-commits";
|
||||
export * from "./folder-tree-checkpoint-resources";
|
||||
export * from "./folder-tree-checkpoints";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
|
||||
@@ -152,10 +152,16 @@ export enum TableName {
|
||||
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||
SecretReminderRecipients = "secret_reminder_recipients",
|
||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||
GithubOrgSyncConfig = "github_org_sync_configs",
|
||||
FolderCommit = "folder_commits",
|
||||
FolderCommitChanges = "folder_commit_changes",
|
||||
FolderCheckpoint = "folder_checkpoints",
|
||||
FolderCheckpointResources = "folder_checkpoint_resources",
|
||||
FolderTreeCheckpoint = "folder_tree_checkpoints",
|
||||
FolderTreeCheckpointResources = "folder_tree_checkpoint_resources"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||
|
||||
export const UserDeviceSchema = z
|
||||
.object({
|
||||
|
||||
@@ -18,6 +18,7 @@ import { registerLdapRouter } from "./ldap-router";
|
||||
import { registerLicenseRouter } from "./license-router";
|
||||
import { registerOidcRouter } from "./oidc-router";
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
import { registerPITRouter } from "./pit-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
import { registerProjectRouter } from "./project-router";
|
||||
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||
@@ -53,6 +54,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
|
||||
await server.register(registerPITRouter, { prefix: "/pit" });
|
||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
await server.register(registerSecretApprovalRequestRouter, {
|
||||
prefix: "/secret-approval-requests"
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
||||
|
||||
const commitHistoryItemSchema = z.object({
|
||||
id: z.string(),
|
||||
folderId: z.string(),
|
||||
actorType: z.string(),
|
||||
actorMetadata: z.unknown().optional(),
|
||||
message: z.string().optional().nullable(),
|
||||
commitId: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
envId: z.string()
|
||||
});
|
||||
|
||||
const versionSchema = z.object({
|
||||
secretKey: z.string().optional(),
|
||||
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()
|
||||
});
|
||||
|
||||
const folderStateSchema = z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
versionId: z.string(),
|
||||
secretKey: z.string().optional(),
|
||||
secretVersion: z.number().optional(),
|
||||
folderName: z.string().optional(),
|
||||
folderVersion: z.number().optional()
|
||||
})
|
||||
);
|
||||
|
||||
export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
// Get commits count for a folder
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/count/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
count: z.number(),
|
||||
folderId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const res = await server.services.folderCommit.getCommitsCount({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
path: req.query.path,
|
||||
commitCount: res.count.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// Get all commits for a folder
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: commitHistoryItemSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const commits = await server.services.folderCommit.getCommitsForFolder({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMITS,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
path: req.query.path,
|
||||
commitCount: commits.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return commits.map((commit) => ({
|
||||
...commit,
|
||||
commitId: commit.commitId.toString()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Get commit changes for a specific commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId/:commitId/changes",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
changes: 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().optional().nullable(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
updatedAt: z.string().or(z.date()),
|
||||
changes: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
folderCommitId: z.string(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
secretVersionId: z.string().optional().nullable(),
|
||||
folderVersionId: z.string().optional().nullable(),
|
||||
// Fix these two fields to accept either string or Date objects
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
folderName: z.string().optional().nullable(),
|
||||
folderChangeId: z.string().optional().nullable(),
|
||||
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretId: z.string().optional().nullable(),
|
||||
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().nullable(),
|
||||
versions: z.array(versionSchema).optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const changes = await server.services.folderCommit.getCommitChanges({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
commitId: req.params.commitId
|
||||
});
|
||||
for (const change of changes.changes) {
|
||||
if (change.secretVersionId) {
|
||||
const currentVersion = change.secretVersion || "1";
|
||||
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
|
||||
if (change.secretId) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||
actorId: req.permission?.id,
|
||||
actor: req.permission?.type,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
secretId: change.secretId,
|
||||
secretVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion],
|
||||
folderId: change.folderId
|
||||
});
|
||||
change.versions = versions?.map((v) => ({
|
||||
secretKey: v.secretKey,
|
||||
secretComment: v.secretComment,
|
||||
skipMultilineEncoding: v.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: v.secretReminderRepeatDays,
|
||||
secretReminderNote: v.secretReminderNote,
|
||||
metadata: v.secretMetadata,
|
||||
tags: v.tags?.map((t) => t.name),
|
||||
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
|
||||
secretValue: v.secretValue
|
||||
}));
|
||||
}
|
||||
} else if (change.folderVersionId && change.folderChangeId) {
|
||||
const currentVersion = change.folderVersion || "1";
|
||||
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||
folderId: change.folderChangeId,
|
||||
folderVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion]
|
||||
});
|
||||
change.versions = versions.map((v) => ({
|
||||
name: v.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
changesCount: (changes.changes?.length || 0).toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
changes: {
|
||||
...changes,
|
||||
commitId: changes.commitId.toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve rollback changes for a commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId/:commitId/compare",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
folderId: z.string().trim(),
|
||||
envId: z.string().trim(),
|
||||
deepRollback: booleanSchema.default(false),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
folderId: z.string(),
|
||||
folderName: z.string(),
|
||||
folderPath: z.string().optional(),
|
||||
changes: z.any()
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
||||
folderId: req.query.folderId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
if (!latestCommit) {
|
||||
throw new NotFoundError({ message: "Latest commit not found" });
|
||||
}
|
||||
|
||||
let diffs;
|
||||
if (req.query.deepRollback) {
|
||||
diffs = await server.services.folderCommit.deepCompareFolder({
|
||||
targetCommitId: req.params.commitId,
|
||||
envId: req.query.envId,
|
||||
actorId: req.permission?.id,
|
||||
actorType: req.permission?.type,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
} else {
|
||||
const folder = await server.services.folder.getFolderById({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
id: req.query.folderId
|
||||
});
|
||||
diffs = [
|
||||
{
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
folderPath: req.query.secretPath,
|
||||
changes: await server.services.folderCommit.compareFolderStates({
|
||||
targetCommitId: req.params.commitId,
|
||||
currentCommitId: latestCommit.id
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
for (const diff of diffs) {
|
||||
for (const change of diff.changes) {
|
||||
if (change.secretKey) {
|
||||
const currentVersion = change.secretVersion || "1";
|
||||
const previousVersion = change.fromVersion || "1";
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||
actorId: req.permission?.id,
|
||||
actor: req.permission?.type,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
secretId: change.id,
|
||||
// if it's update add also the previous secretversionid
|
||||
secretVersions:
|
||||
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion],
|
||||
folderId: req.query.folderId
|
||||
});
|
||||
change.versions = versions?.map((v) => ({
|
||||
secretKey: v.secretKey,
|
||||
secretComment: v.secretComment,
|
||||
skipMultilineEncoding: v.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: v.secretReminderRepeatDays,
|
||||
secretReminderNote: v.secretReminderNote,
|
||||
metadata: v.metadata,
|
||||
tags: v.tags?.map((t) => t.name),
|
||||
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
|
||||
secretValue: v.secretValue
|
||||
}));
|
||||
}
|
||||
if (change.folderVersion) {
|
||||
const currentVersion = change.folderVersion || "1";
|
||||
const previousVersion = change.fromVersion || "1";
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||
folderId: change.id,
|
||||
folderVersions:
|
||||
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion]
|
||||
});
|
||||
change.versions = versions.map((v) => ({
|
||||
name: v.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_COMPARE_FOLDER_STATES,
|
||||
metadata: {
|
||||
targetCommitId: req.params.commitId,
|
||||
folderId: req.query.folderId,
|
||||
deepRollback: req.query.deepRollback,
|
||||
diffsCount: diffs.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return diffs;
|
||||
}
|
||||
});
|
||||
|
||||
// Rollback to a previous commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/commits/:workspaceId/:commitId/rollback",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
folderId: z.string().trim(),
|
||||
deepRollback: z.boolean().default(false),
|
||||
message: z.string().trim().optional(),
|
||||
envId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
secretChangesCount: z.number().optional(),
|
||||
folderChangesCount: z.number().optional(),
|
||||
totalChanges: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
||||
folderId: req.body.folderId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
if (!latestCommit) {
|
||||
throw new NotFoundError({ message: "Latest commit not found" });
|
||||
}
|
||||
|
||||
if (req.body.deepRollback) {
|
||||
await server.services.folderCommit.deepRollbackFolder(
|
||||
req.params.commitId,
|
||||
req.body.envId,
|
||||
req.permission.id,
|
||||
req.permission.type,
|
||||
req.params.workspaceId
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
const diff = await server.services.folderCommit.compareFolderStates({
|
||||
currentCommitId: latestCommit.id,
|
||||
targetCommitId: req.params.commitId
|
||||
});
|
||||
|
||||
const response = await server.services.folderCommit.applyFolderStateDifferences({
|
||||
differences: diff,
|
||||
actorInfo: {
|
||||
actorType: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
message: req.body.message || "Rollback to previous commit"
|
||||
},
|
||||
folderId: req.body.folderId,
|
||||
projectId: req.params.workspaceId,
|
||||
reconstructNewFolders: req.body.deepRollback
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_ROLLBACK_COMMIT,
|
||||
metadata: {
|
||||
targetCommitId: req.params.commitId,
|
||||
folderId: req.body.folderId,
|
||||
deepRollback: req.body.deepRollback,
|
||||
message: req.body.message || "Rollback to previous commit",
|
||||
totalChanges: response.totalChanges.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
secretChangesCount: response.secretChangesCount,
|
||||
folderChangesCount: response.folderChangesCount,
|
||||
totalChanges: response.totalChanges
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Revert commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/commits/:workspaceId/:commitId/revert",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
originalCommitId: z.string(),
|
||||
revertCommitId: z.string().optional(),
|
||||
changesReverted: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.folderCommit.revertCommitChanges({
|
||||
commitId: req.params.commitId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_REVERT_COMMIT,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
revertCommitId: response.revertCommitId,
|
||||
changesReverted: response.changesReverted?.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
// Folder state at commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId/:commitId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
folderId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: folderStateSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_GET_FOLDER_STATE,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
folderId: req.query.folderId,
|
||||
resourceCount: response.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response.map((item) => ({
|
||||
...item,
|
||||
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
|
||||
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -343,7 +343,15 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end",
|
||||
|
||||
GET_PROJECT_PIT_COMMITS = "get-project-pit-commits",
|
||||
GET_PROJECT_PIT_COMMIT_CHANGES = "get-project-pit-commit-changes",
|
||||
GET_PROJECT_PIT_COMMIT_COUNT = "get-project-pit-commit-count",
|
||||
PIT_ROLLBACK_COMMIT = "pit-rollback-commit",
|
||||
PIT_REVERT_COMMIT = "pit-revert-commit",
|
||||
PIT_GET_FOLDER_STATE = "pit-get-folder-state",
|
||||
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -2694,6 +2702,71 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitsEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMITS;
|
||||
metadata: {
|
||||
commitCount: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitChangesEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES;
|
||||
metadata: {
|
||||
changesCount: string;
|
||||
commitId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitCountEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT;
|
||||
metadata: {
|
||||
environment: string;
|
||||
path: string;
|
||||
commitCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitRollbackCommitEvent {
|
||||
type: EventType.PIT_ROLLBACK_COMMIT;
|
||||
metadata: {
|
||||
targetCommitId: string;
|
||||
folderId: string;
|
||||
deepRollback: boolean;
|
||||
message: string;
|
||||
totalChanges: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitRevertCommitEvent {
|
||||
type: EventType.PIT_REVERT_COMMIT;
|
||||
metadata: {
|
||||
commitId: string;
|
||||
revertCommitId?: string;
|
||||
changesReverted?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitGetFolderStateEvent {
|
||||
type: EventType.PIT_GET_FOLDER_STATE;
|
||||
metadata: {
|
||||
commitId: string;
|
||||
folderId: string;
|
||||
resourceCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitCompareFolderStatesEvent {
|
||||
type: EventType.PIT_COMPARE_FOLDER_STATES;
|
||||
metadata: {
|
||||
targetCommitId: string;
|
||||
folderId: string;
|
||||
deepRollback: boolean;
|
||||
diffsCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -2941,4 +3014,11 @@ export type Event =
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||
| GetProjectPitCommitsEvent
|
||||
| GetProjectPitCommitChangesEvent
|
||||
| PitRollbackCommitEvent
|
||||
| GetProjectPitCommitCountEvent
|
||||
| PitRevertCommitEvent
|
||||
| PitCompareFolderStatesEvent
|
||||
| PitGetFolderStateEvent;
|
||||
|
||||
@@ -17,6 +17,11 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCommitsActions {
|
||||
Read = "read",
|
||||
PerformRollback = "perform-rollback"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeAndReadValue = "read",
|
||||
DescribeSecret = "describeSecret",
|
||||
@@ -126,6 +131,7 @@ export enum ProjectPermissionSub {
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Commits = "commits",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates",
|
||||
@@ -251,7 +257,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -557,6 +564,12 @@ const GeneralPermissionSchema = [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Commits).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCommitsActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
@@ -829,6 +842,11 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -1011,6 +1029,11 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -1046,6 +1069,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { EnforcementLevel } from "@app/lib/types";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
@@ -130,6 +131,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@@ -161,7 +163,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
projectSlackConfigDAL,
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@@ -596,6 +599,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
? await fnSecretV2BridgeBulkInsert({
|
||||
tx,
|
||||
folderId,
|
||||
actor: {
|
||||
actorId,
|
||||
type: actor
|
||||
},
|
||||
orgId: actorOrgId,
|
||||
inputSecrets: secretCreationCommits.map((el) => ({
|
||||
tagIds: el?.tags.map(({ id }) => id),
|
||||
@@ -618,13 +625,18 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
folderCommitService
|
||||
})
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await fnSecretV2BridgeBulkUpdate({
|
||||
folderId,
|
||||
orgId: actorOrgId,
|
||||
actor: {
|
||||
actorId,
|
||||
type: actor
|
||||
},
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
@@ -658,7 +670,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
})
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
@@ -666,10 +679,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
projectId,
|
||||
folderId,
|
||||
tx,
|
||||
actorId: "",
|
||||
actorId,
|
||||
actorType: actor,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
|
||||
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })),
|
||||
folderCommitService,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL
|
||||
})
|
||||
: [];
|
||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||
|
||||
@@ -10,6 +10,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -87,6 +88,7 @@ type TSecretReplicationServiceFactoryDep = {
|
||||
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
|
||||
@@ -132,6 +134,7 @@ export const secretReplicationServiceFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
kmsService,
|
||||
folderCommitService,
|
||||
resourceMetadataDAL
|
||||
}: TSecretReplicationServiceFactoryDep) => {
|
||||
const $getReplicatedSecrets = (
|
||||
@@ -446,6 +449,7 @@ export const secretReplicationServiceFactory = ({
|
||||
tx,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||
return {
|
||||
@@ -466,6 +470,7 @@ export const secretReplicationServiceFactory = ({
|
||||
orgId,
|
||||
folderId: destinationReplicationFolderId,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
folderCommitService,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
tx,
|
||||
resourceMetadataDAL,
|
||||
|
||||
@@ -61,6 +61,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -96,7 +97,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
||||
TSecretV2BridgeDALFactory,
|
||||
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
||||
@@ -104,6 +105,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
@@ -141,6 +143,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
snapshotService,
|
||||
keyStore,
|
||||
queueService,
|
||||
folderCommitService,
|
||||
appConnectionDAL
|
||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
||||
@@ -533,7 +536,12 @@ export const secretRotationV2ServiceFactory = ({
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL
|
||||
folderCommitService,
|
||||
resourceMetadataDAL,
|
||||
actor: {
|
||||
type: actor.type,
|
||||
actorId: actor.id
|
||||
}
|
||||
});
|
||||
|
||||
await secretRotationV2DAL.insertSecretMappings(
|
||||
@@ -668,7 +676,12 @@ export const secretRotationV2ServiceFactory = ({
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL
|
||||
folderCommitService,
|
||||
resourceMetadataDAL,
|
||||
actor: {
|
||||
type: actor.type,
|
||||
actorId: actor.id
|
||||
}
|
||||
});
|
||||
|
||||
secretsMappingUpdated = true;
|
||||
@@ -786,6 +799,9 @@ export const secretRotationV2ServiceFactory = ({
|
||||
projectId,
|
||||
folderId,
|
||||
actorId: actor.id, // not actually used since rotated secrets are shared
|
||||
actorType: actor.type,
|
||||
folderCommitService,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
@@ -926,6 +942,10 @@ export const secretRotationV2ServiceFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
folderCommitService,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM
|
||||
},
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
+20
-1
@@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -53,6 +54,7 @@ type TSecretRotationQueueFactoryDep = {
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
// These error should stop the repeatable job and ask user to reconfigure rotation
|
||||
@@ -77,6 +79,7 @@ export const secretRotationQueueFactory = ({
|
||||
telemetryService,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
folderCommitService,
|
||||
kmsService
|
||||
}: TSecretRotationQueueFactoryDep) => {
|
||||
const addToQueue = async (rotationId: string, interval: number) => {
|
||||
@@ -330,7 +333,7 @@ export const secretRotationQueueFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretVersionV2BridgeDAL.insertMany(
|
||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
actorType: ActorType.PLATFORM,
|
||||
@@ -338,6 +341,22 @@ export const secretRotationQueueFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: ActorType.PLATFORM
|
||||
},
|
||||
message: "Changed by Secret rotation",
|
||||
folderId: secretVersions[0].folderId,
|
||||
changes: secretVersions.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}))
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
|
||||
|
||||
@@ -228,6 +228,10 @@ const envSchema = z
|
||||
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||
|
||||
// PIT
|
||||
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("10")),
|
||||
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
CORS_ALLOWED_ORIGINS: zpStr(
|
||||
|
||||
@@ -49,7 +49,8 @@ export enum QueueName {
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||
SecretRotationV2 = "secret-rotation-v2"
|
||||
SecretRotationV2 = "secret-rotation-v2",
|
||||
FolderTreeCheckpoint = "folder-tree-checkpoint"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@@ -81,7 +82,8 @@ export enum QueueJobs {
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
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 = {
|
||||
@@ -191,6 +193,12 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.ProjectV3Migration;
|
||||
payload: { projectId: string };
|
||||
};
|
||||
[QueueName.FolderTreeCheckpoint]: {
|
||||
name: QueueJobs.CreateFolderTreeCheckpoint;
|
||||
payload: {
|
||||
envId: string;
|
||||
};
|
||||
};
|
||||
[QueueName.ImportSecretsFromExternalSource]: {
|
||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||
payload: {
|
||||
|
||||
@@ -140,6 +140,14 @@ import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-gr
|
||||
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
|
||||
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
||||
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-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 { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-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 { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
@@ -551,6 +559,37 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
||||
const folderCheckpointDAL = folderCheckpointDALFactory(db);
|
||||
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
||||
const folderCommitDAL = folderCommitDALFactory(db);
|
||||
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||
const folderCommitQueueService = folderCommitQueueServiceFactory({
|
||||
queueService,
|
||||
folderTreeCheckpointDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitDAL,
|
||||
folderDAL
|
||||
});
|
||||
const folderCommitService = folderCommitServiceFactory({
|
||||
folderCommitDAL,
|
||||
folderCommitChangesDAL,
|
||||
folderCheckpointDAL,
|
||||
folderTreeCheckpointDAL,
|
||||
userDAL,
|
||||
identityDAL,
|
||||
folderDAL,
|
||||
folderVersionDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
projectDAL,
|
||||
folderCheckpointResourcesDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitQueueService,
|
||||
permissionService
|
||||
});
|
||||
const scimService = scimServiceFactory({
|
||||
licenseService,
|
||||
scimDAL,
|
||||
@@ -973,6 +1012,7 @@ export const registerRoutes = async (
|
||||
projectMembershipDAL,
|
||||
projectBotDAL,
|
||||
secretDAL,
|
||||
folderCommitService,
|
||||
secretBlindIndexDAL,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
@@ -1019,6 +1059,7 @@ export const registerRoutes = async (
|
||||
secretReminderRecipientsDAL,
|
||||
orgService,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretSyncQueue
|
||||
});
|
||||
|
||||
@@ -1120,7 +1161,8 @@ export const registerRoutes = async (
|
||||
folderVersionDAL,
|
||||
projectEnvDAL,
|
||||
snapshotService,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const secretImportService = secretImportServiceFactory({
|
||||
@@ -1145,6 +1187,7 @@ export const registerRoutes = async (
|
||||
const secretV2BridgeService = secretV2BridgeServiceFactory({
|
||||
folderDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
folderCommitService,
|
||||
secretQueueService,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
permissionService,
|
||||
@@ -1188,7 +1231,8 @@ export const registerRoutes = async (
|
||||
projectSlackConfigDAL,
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@@ -1273,7 +1317,8 @@ export const registerRoutes = async (
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const secretRotationQueue = secretRotationQueueFactory({
|
||||
@@ -1285,6 +1330,7 @@ export const registerRoutes = async (
|
||||
projectBotService,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderCommitService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
@@ -1557,7 +1603,9 @@ export const registerRoutes = async (
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
queueService,
|
||||
secretV2BridgeService,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL
|
||||
});
|
||||
|
||||
const migrationService = externalMigrationServiceFactory({
|
||||
@@ -1619,6 +1667,7 @@ export const registerRoutes = async (
|
||||
auditLogService,
|
||||
secretV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
folderCommitService,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
keyStore,
|
||||
@@ -1746,7 +1795,8 @@ export const registerRoutes = async (
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
microsoftTeams: microsoftTeamsService,
|
||||
assumePrivileges: assumePrivilegeService,
|
||||
githubOrgSync: githubOrgSyncConfigService
|
||||
githubOrgSync: githubOrgSyncConfigService,
|
||||
folderCommit: folderCommitService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -10,6 +10,7 @@ import { chunkArray } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
@@ -18,6 +19,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||
@@ -42,6 +44,8 @@ export type TImportDataIntoInfisicalDTO = {
|
||||
projectService: Pick<TProjectServiceFactory, "createProject">;
|
||||
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
||||
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||
|
||||
input: TImportInfisicalDataCreate;
|
||||
};
|
||||
@@ -507,6 +511,8 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretVersionTagDAL,
|
||||
folderDAL,
|
||||
resourceMetadataDAL,
|
||||
folderVersionDAL,
|
||||
folderCommitService,
|
||||
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||
}: TImportDataIntoInfisicalDTO) => {
|
||||
// Import data to infisical
|
||||
@@ -599,6 +605,36 @@ export const importDataIntoInfisicalFn = async ({
|
||||
tx
|
||||
);
|
||||
|
||||
const newFolderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: newFolder.name,
|
||||
envId: newFolder.envId,
|
||||
version: newFolder.version,
|
||||
folderId: newFolder.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Changed by external migration",
|
||||
folderId: parentEnv.rootFolderId,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
folderVersionId: newFolderVersion.id
|
||||
}
|
||||
]
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
originalToNewFolderId.set(folder.id, {
|
||||
folderId: newFolder.id,
|
||||
projectId: parentEnv.projectId
|
||||
@@ -772,6 +808,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
folderCommitService,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
|
||||
@@ -3,6 +3,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
@@ -10,6 +11,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
||||
@@ -36,6 +38,8 @@ export type TExternalMigrationQueueFactoryDep = {
|
||||
projectService: Pick<TProjectServiceFactory, "createProject">;
|
||||
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
||||
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
@@ -56,6 +60,8 @@ export const externalMigrationQueueFactory = ({
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
folderDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL,
|
||||
resourceMetadataDAL
|
||||
}: TExternalMigrationQueueFactoryDep) => {
|
||||
const startImport = async (dto: {
|
||||
@@ -114,6 +120,8 @@ export const externalMigrationQueueFactory = ({
|
||||
projectService,
|
||||
projectEnvService,
|
||||
secretV2BridgeService,
|
||||
folderCommitService,
|
||||
folderVersionDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TFolderCheckpointResources,
|
||||
TFolderCheckpoints,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCheckpointResourcesDALFactory = ReturnType<typeof folderCheckpointResourcesDALFactory>;
|
||||
|
||||
export type ResourceWithCheckpointInfo = TFolderCheckpointResources & {
|
||||
folderCommitId: string;
|
||||
};
|
||||
|
||||
export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
const folderCheckpointResourcesOrm = ormify(db, TableName.FolderCheckpointResources);
|
||||
|
||||
const findByCheckpointId = async (
|
||||
folderCheckpointId: string,
|
||||
tx?: Knex
|
||||
): Promise<
|
||||
(TFolderCheckpointResources & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderCheckpointResources>(TableName.FolderCheckpointResources)
|
||||
.where({ folderCheckpointId })
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCheckpointResources}.secretVersionId`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.leftJoin<TSecretFolderVersions>(
|
||||
TableName.SecretFolderVersion,
|
||||
`${TableName.FolderCheckpointResources}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
|
||||
);
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCheckpointId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
|
||||
>(TableName.FolderCheckpointResources)
|
||||
.where({ secretVersionId })
|
||||
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||
.join(
|
||||
TableName.FolderCheckpoint,
|
||||
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
|
||||
`${TableName.FolderCheckpoint}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
|
||||
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
|
||||
>(TableName.FolderCheckpointResources)
|
||||
.where({ folderVersionId })
|
||||
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||
.join(
|
||||
TableName.FolderCheckpoint,
|
||||
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
|
||||
`${TableName.FolderCheckpoint}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
|
||||
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...folderCheckpointResourcesOrm,
|
||||
findByCheckpointId,
|
||||
findBySecretVersionId,
|
||||
findByFolderVersionId
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TFolderCheckpoints, TFolderCommits } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCheckpointDALFactory = ReturnType<typeof folderCheckpointDALFactory>;
|
||||
|
||||
type CheckpointWithCommitInfo = TFolderCheckpoints & {
|
||||
actorMetadata: unknown;
|
||||
actorType: string;
|
||||
message?: string | null;
|
||||
commitDate: Date;
|
||||
folderId: string;
|
||||
};
|
||||
|
||||
export const folderCheckpointDALFactory = (db: TDbClient) => {
|
||||
const folderCheckpointOrm = ormify(db, TableName.FolderCheckpoint);
|
||||
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderCheckpoints | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())<TFolderCheckpoints>(TableName.FolderCheckpoint)
|
||||
.where({ folderCommitId })
|
||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderId = async (folderId: string, limit?: number, tx?: Knex): Promise<CheckpointWithCommitInfo[]> => {
|
||||
try {
|
||||
let query = (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||
.join<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||
.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.FolderCheckpoint}.createdAt`, "desc");
|
||||
|
||||
if (limit !== undefined) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
return await query;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findLatestByFolderId = async (folderId: string, tx?: Knex): Promise<CheckpointWithCommitInfo | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||
.join<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||
.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.FolderCheckpoint}.createdAt`, "desc")
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestCheckpoint = async (
|
||||
folderCommitId: string,
|
||||
tx?: Knex
|
||||
): Promise<(CheckpointWithCommitInfo & { commitId: number }) | undefined> => {
|
||||
try {
|
||||
// First, get the commit info to find the folder ID and commit sequence number
|
||||
const commit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ id: folderCommitId })
|
||||
.select("commitId", "folderId")
|
||||
.first();
|
||||
|
||||
if (!commit) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the checkpoint with the highest commitId that's still less than or equal to our commit
|
||||
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||
.join<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
.where(`${TableName.FolderCommit}.folderId`, commit.folderId)
|
||||
.where(`${TableName.FolderCommit}.commitId`, "<=", commit.commitId.toString())
|
||||
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||
.select(
|
||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||
db.ref("message").withSchema(TableName.FolderCommit),
|
||||
db.ref("commitId").withSchema(TableName.FolderCommit),
|
||||
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||
)
|
||||
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||
.first();
|
||||
|
||||
return nearestCheckpoint;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...folderCheckpointOrm,
|
||||
findByCommitId,
|
||||
findByFolderId,
|
||||
findLatestByFolderId,
|
||||
findNearestCheckpoint
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommits,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||
|
||||
type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
actorMetadata: unknown;
|
||||
actorType: string;
|
||||
message?: string | null;
|
||||
folderId: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderChangeId?: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean | null;
|
||||
secretReminderRepeatDays?: number | null;
|
||||
secretReminderNote?: string | null;
|
||||
metadata?: unknown;
|
||||
tags?: string[] | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
secretValue?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
|
||||
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||
.where({ folderCommitId })
|
||||
.leftJoin<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderCommitChanges}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.leftJoin<TSecretFolderVersions>(
|
||||
TableName.SecretFolderVersion,
|
||||
`${TableName.FolderCommitChanges}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2),
|
||||
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)
|
||||
);
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCommitChanges &
|
||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||
>(TableName.FolderCommitChanges)
|
||||
.where({ secretVersionId })
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.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)
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCommitChanges &
|
||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||
>(TableName.FolderCommitChanges)
|
||||
.where({ folderVersionId })
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.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)
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...folderCommitChangesOrm,
|
||||
findByCommitId,
|
||||
findBySecretVersionId,
|
||||
findByFolderVersionId
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,361 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommits,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
|
||||
|
||||
export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
const folderCommitOrm = ormify(db, TableName.FolderCommit);
|
||||
const { delete: deleteOp, deleteById, ...restOfOrm } = folderCommitOrm;
|
||||
|
||||
const findByFolderId = async (folderId: string, tx?: Knex): Promise<TFolderCommits[]> => {
|
||||
try {
|
||||
const trx = tx || db.replicaNode();
|
||||
|
||||
// First, get all folder commits
|
||||
const folderCommits = await trx(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.select("*")
|
||||
.orderBy("createdAt", "desc");
|
||||
|
||||
if (folderCommits.length === 0) return [];
|
||||
|
||||
// Get all commit IDs
|
||||
const commitIds = folderCommits.map((commit) => commit.id);
|
||||
|
||||
// Then get all related changes
|
||||
const changes = await trx(TableName.FolderCommitChanges).whereIn("folderCommitId", commitIds).select("*");
|
||||
|
||||
const changesMap = changes.reduce(
|
||||
(acc, change) => {
|
||||
const { folderCommitId } = change;
|
||||
if (!acc[folderCommitId]) acc[folderCommitId] = [];
|
||||
acc[folderCommitId].push(change);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TFolderCommitChanges[]>
|
||||
);
|
||||
|
||||
return folderCommits.map((commit) => ({
|
||||
...commit,
|
||||
changes: changesMap[commit.id] || []
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findLatestCommit = async (folderId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc")
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestCommit" });
|
||||
}
|
||||
};
|
||||
|
||||
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> => {
|
||||
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({ folderId })
|
||||
.where("commitId", ">", referencedCommit.commitId)
|
||||
.count();
|
||||
return Number(doc?.[0].count);
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
folderId: string,
|
||||
targetCommitNumber: number,
|
||||
checkpointCommitNumber: number,
|
||||
tx?: Knex
|
||||
): Promise<
|
||||
(TFolderCommits & {
|
||||
changes: (TFolderCommitChanges & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[];
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
// First get all the commits in the range
|
||||
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber)
|
||||
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber)
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
|
||||
|
||||
// If no commits found, return empty array
|
||||
if (!commits.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all the commit IDs
|
||||
const commitIds = commits.map((commit) => commit.id);
|
||||
|
||||
// Get all changes for these commits in a single query
|
||||
const allChanges = await (tx || db.replicaNode())(TableName.FolderCommitChanges)
|
||||
.whereIn("folderCommitId", commitIds)
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.leftJoin<TSecretFolderVersions>(
|
||||
TableName.SecretFolderVersion,
|
||||
`${TableName.FolderCommitChanges}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
|
||||
);
|
||||
|
||||
// Organize changes by commit ID
|
||||
const changesByCommitId = allChanges.reduce(
|
||||
(acc, change) => {
|
||||
if (!acc[change.folderCommitId]) {
|
||||
acc[change.folderCommitId] = [];
|
||||
}
|
||||
acc[change.folderCommitId].push(change);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TFolderCommitChanges[]>
|
||||
);
|
||||
|
||||
// Attach changes to each commit
|
||||
return commits.map((commit) => ({
|
||||
...commit,
|
||||
changes: changesByCommitId[commit.id] || []
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindCommitsToRecreate" });
|
||||
}
|
||||
};
|
||||
|
||||
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" });
|
||||
}
|
||||
};
|
||||
|
||||
const findAllFolderCommitsAfter = async ({
|
||||
envId,
|
||||
startCommitId,
|
||||
tx
|
||||
}: {
|
||||
folderId?: string;
|
||||
envId?: string;
|
||||
startCommitId?: string;
|
||||
tx?: Knex;
|
||||
}): Promise<TFolderCommits[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.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 {
|
||||
...restOfOrm,
|
||||
findByFolderId,
|
||||
findLatestCommit,
|
||||
getNumberOfCommitsSince,
|
||||
findCommitsToRecreate,
|
||||
findMultipleLatestCommits,
|
||||
findAllCommitsBetween,
|
||||
findLatestCommitBetween,
|
||||
findLatestEnvCommit,
|
||||
getEnvNumberOfCommitsSince,
|
||||
findLatestCommitByFolderIds,
|
||||
findAllFolderCommitsAfter
|
||||
};
|
||||
};
|
||||
@@ -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,608 @@
|
||||
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({}),
|
||||
findLatestVersionMany: 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({})
|
||||
};
|
||||
|
||||
const mockPermissionService = {
|
||||
getProjectPermission: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
let folderCommitService: TFolderCommitServiceFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
folderCommitService = folderCommitServiceFactory({
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCommitDAL: mockFolderCommitDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCommitChangesDAL: mockFolderCommitChangesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCheckpointDAL: mockFolderCheckpointDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCheckpointResourcesDAL: mockFolderCheckpointResourcesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderTreeCheckpointDAL: mockFolderTreeCheckpointDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderTreeCheckpointResourcesDAL: mockFolderTreeCheckpointResourcesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
userDAL: mockUserDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
identityDAL: mockIdentityDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderDAL: mockFolderDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderVersionDAL: mockFolderVersionDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
secretVersionV2BridgeDAL: mockSecretVersionV2BridgeDAL,
|
||||
projectDAL: mockProjectDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
secretV2BridgeDAL: mockSecretV2BridgeDAL,
|
||||
folderCommitQueueService: mockFolderCommitQueueService,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
permissionService: mockPermissionService
|
||||
});
|
||||
});
|
||||
|
||||
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(Object.values(secretVersions));
|
||||
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(Object.values(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" });
|
||||
mockSecretVersionV2BridgeDAL.findLatestVersionMany.mockResolvedValue([
|
||||
{
|
||||
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: {}
|
||||
}
|
||||
]);
|
||||
|
||||
// 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
+44
@@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TFolderTreeCheckpointResources } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderTreeCheckpointResourcesDALFactory = ReturnType<typeof folderTreeCheckpointResourcesDALFactory>;
|
||||
|
||||
type TFolderTreeCheckpointResourcesWithCommitId = TFolderTreeCheckpointResources & {
|
||||
commitId: number;
|
||||
};
|
||||
|
||||
export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
const folderTreeCheckpointResourcesOrm = ormify(db, TableName.FolderTreeCheckpointResources);
|
||||
|
||||
const findByTreeCheckpointId = async (
|
||||
folderTreeCheckpointId: string,
|
||||
tx?: Knex
|
||||
): Promise<TFolderTreeCheckpointResourcesWithCommitId[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderTreeCheckpointResources>(
|
||||
TableName.FolderTreeCheckpointResources
|
||||
)
|
||||
.join(
|
||||
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;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByTreeCheckpointId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...folderTreeCheckpointResourcesOrm,
|
||||
findByTreeCheckpointId
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TFolderCommits, TFolderTreeCheckpoints } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderTreeCheckpointDALFactory = ReturnType<typeof folderTreeCheckpointDALFactory>;
|
||||
|
||||
type TreeCheckpointWithCommitInfo = TFolderTreeCheckpoints & {
|
||||
commitId: number;
|
||||
};
|
||||
|
||||
export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
||||
const folderTreeCheckpointOrm = ormify(db, TableName.FolderTreeCheckpoint);
|
||||
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
|
||||
.where({ folderCommitId })
|
||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestCheckpoint = async (
|
||||
folderCommitId: string,
|
||||
envId: string,
|
||||
tx?: Knex
|
||||
): Promise<TreeCheckpointWithCommitInfo | undefined> => {
|
||||
try {
|
||||
const targetCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ id: folderCommitId })
|
||||
.select("id", "commitId", "folderId")
|
||||
.first();
|
||||
|
||||
if (!targetCommit) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint)
|
||||
.join<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
.where(`${TableName.FolderCommit}.commitId`, "<=", targetCommit.commitId.toString())
|
||||
.where(`${TableName.FolderCommit}.envId`, envId)
|
||||
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
||||
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||
.first();
|
||||
|
||||
return nearestCheckpoint;
|
||||
} 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")
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...folderTreeCheckpointOrm,
|
||||
findByCommitId,
|
||||
findNearestCheckpoint,
|
||||
findLatestByEnvId
|
||||
};
|
||||
};
|
||||
@@ -488,6 +488,73 @@ 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) => {
|
||||
try {
|
||||
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||
.where({ parentId })
|
||||
.select(selectAllTableCols(TableName.SecretFolder));
|
||||
return folders;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findByParentId" });
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
...secretFolderOrm,
|
||||
update,
|
||||
@@ -499,6 +566,9 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
findClosestFolder,
|
||||
findByProjectId,
|
||||
findByMultiEnv,
|
||||
findByEnvsDeep
|
||||
findByEnvsDeep,
|
||||
findByParentId,
|
||||
findByEnvId,
|
||||
findFoldersByRootAndIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +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 { 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";
|
||||
@@ -29,7 +30,8 @@ type TSecretFolderServiceFactoryDep = {
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
};
|
||||
|
||||
@@ -41,6 +43,7 @@ export const secretFolderServiceFactory = ({
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
folderVersionDAL,
|
||||
folderCommitService,
|
||||
projectDAL
|
||||
}: TSecretFolderServiceFactoryDep) => {
|
||||
const createFolder = async ({
|
||||
@@ -111,7 +114,7 @@ export const secretFolderServiceFactory = ({
|
||||
});
|
||||
parentFolderId = newFolders.at(-1)?.id as string;
|
||||
const docs = await folderDAL.insertMany(newFolders, tx);
|
||||
await folderVersionDAL.insertMany(
|
||||
const folderVersions = await folderVersionDAL.insertMany(
|
||||
docs.map((doc) => ({
|
||||
name: doc.name,
|
||||
envId: doc.envId,
|
||||
@@ -120,6 +123,23 @@ export const secretFolderServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
changes: folderVersions.map((fv) => ({
|
||||
type: "add",
|
||||
folderVersionId: fv.id
|
||||
}))
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +147,7 @@ export const secretFolderServiceFactory = ({
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
const folderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
envId: doc.envId,
|
||||
@@ -136,6 +156,25 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: parentFolderId,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
|
||||
@@ -225,7 +264,7 @@ export const secretFolderServiceFactory = ({
|
||||
{ name, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
const folderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
envId: doc.envId,
|
||||
@@ -234,6 +273,26 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Folder updated",
|
||||
folderId: parentFolder.id,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!doc) {
|
||||
throw new NotFoundError({
|
||||
message: `Failed to update folder with id '${id}', not found`,
|
||||
@@ -321,7 +380,7 @@ export const secretFolderServiceFactory = ({
|
||||
{ name, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
const folderVersion = await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
envId: doc.envId,
|
||||
@@ -330,6 +389,26 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Folder updated",
|
||||
folderId: parentFolder.id,
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!doc) throw new NotFoundError({ message: `Failed to update folder with ID '${id}'`, name: "UpdateFolder" });
|
||||
return doc;
|
||||
});
|
||||
@@ -381,7 +460,30 @@ export const secretFolderServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
|
||||
|
||||
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id]);
|
||||
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actor,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Folder deleted",
|
||||
folderId: parentFolder.id,
|
||||
changes: [
|
||||
{
|
||||
type: "delete",
|
||||
folderVersionId: folderVersions[doc.id].id
|
||||
}
|
||||
]
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
|
||||
@@ -665,6 +767,22 @@ export const secretFolderServiceFactory = ({
|
||||
return environmentFolders;
|
||||
};
|
||||
|
||||
const getFolderVersionsByIds = async ({
|
||||
folderId,
|
||||
folderVersions
|
||||
}: {
|
||||
folderId: string;
|
||||
folderVersions: string[];
|
||||
}) => {
|
||||
const versions = await folderVersionDAL.find({
|
||||
folderId,
|
||||
$in: {
|
||||
version: folderVersions.map((v) => Number.parseInt(v, 10))
|
||||
}
|
||||
});
|
||||
return versions;
|
||||
};
|
||||
|
||||
return {
|
||||
createFolder,
|
||||
updateFolder,
|
||||
@@ -675,6 +793,7 @@ export const secretFolderServiceFactory = ({
|
||||
getProjectFolderCount,
|
||||
getFoldersMultiEnv,
|
||||
getFoldersDeepByEnvs,
|
||||
getProjectEnvironmentsFolders
|
||||
getProjectEnvironmentsFolders,
|
||||
getFolderVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
||||
const docs: Array<TSecretFolderVersions & { max: number }> = await (tx || db.replicaNode())(
|
||||
TableName.SecretFolderVersion
|
||||
)
|
||||
.whereIn("folderId", folderIds)
|
||||
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
|
||||
.join(
|
||||
(tx || db)(TableName.SecretFolderVersion)
|
||||
.groupBy("folderId")
|
||||
@@ -97,5 +97,80 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
|
||||
};
|
||||
|
||||
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
|
||||
const findByIdsWithLatestVersion = async (folderIds: string[], versionIds?: string[], tx?: Knex) => {
|
||||
try {
|
||||
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
|
||||
|
||||
const knexInstance = tx || db.replicaNode();
|
||||
let allDocs: Array<TSecretFolderVersions & { max?: number }> = [];
|
||||
|
||||
// Process latest versions for folderIds
|
||||
if (folderIds.length) {
|
||||
const latestVersions: Array<TSecretFolderVersions & { max: number }> = await knexInstance(
|
||||
TableName.SecretFolderVersion
|
||||
)
|
||||
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
|
||||
.join(
|
||||
knexInstance(TableName.SecretFolderVersion)
|
||||
.groupBy("folderId")
|
||||
.max("version")
|
||||
.select("folderId")
|
||||
.as("latestVersion"),
|
||||
(bd) => {
|
||||
bd.on(`${TableName.SecretFolderVersion}.folderId`, "latestVersion.folderId").andOn(
|
||||
`${TableName.SecretFolderVersion}.version`,
|
||||
"latestVersion.max"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
allDocs = [...allDocs, ...latestVersions];
|
||||
}
|
||||
|
||||
// Process specific versions by versionIds
|
||||
if (versionIds && versionIds.length) {
|
||||
// Get the specific versions
|
||||
const specificVersions = await knexInstance(TableName.SecretFolderVersion).whereIn("id", versionIds);
|
||||
|
||||
// Get the folderIds from these versions
|
||||
const specificFolderIds = [...new Set(specificVersions.map((v) => v.folderId).filter(Boolean))];
|
||||
|
||||
// Get max versions for these folderIds
|
||||
const maxVersionsQuery = (await knexInstance(TableName.SecretFolderVersion)
|
||||
.whereIn("folderId", specificFolderIds)
|
||||
.groupBy("folderId")
|
||||
.select("folderId")
|
||||
.max("version as max")) as Array<{ folderId: string; max: number }>;
|
||||
|
||||
// Create a lookup map for max versions
|
||||
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.folderId] = item.max;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Add max version to each specific version
|
||||
const specificVersionsWithMax = specificVersions.map((version) => ({
|
||||
...version,
|
||||
max: maxVersionMap[version.folderId]
|
||||
}));
|
||||
|
||||
allDocs = [...allDocs, ...specificVersionsWithMax];
|
||||
}
|
||||
|
||||
return allDocs.reduce<Record<string, TSecretFolderVersions & { max?: number }>>(
|
||||
(prev, curr) => ({ ...prev, [curr.folderId || ""]: curr }),
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretFolderVerOrm,
|
||||
findLatestFolderVersions,
|
||||
findLatestVersionByFolderId,
|
||||
pruneExcessVersions,
|
||||
findByIdsWithLatestVersion
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
@@ -93,6 +94,7 @@ type TSecretSyncQueueFactoryDep = {
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
type SecretSyncActionJob = Job<
|
||||
@@ -133,7 +135,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretVersionTagDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
}: TSecretSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -164,7 +167,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||
@@ -180,7 +184,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const $getInfisicalSecrets = async (
|
||||
|
||||
@@ -73,6 +73,7 @@ export const fnSecretBulkInsert = async ({
|
||||
resourceMetadataDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
folderCommitService,
|
||||
actor,
|
||||
tx
|
||||
}: TFnSecretBulkInsert) => {
|
||||
@@ -131,6 +132,30 @@ export const fnSecretBulkInsert = async ({
|
||||
tx
|
||||
);
|
||||
|
||||
const commitChanges = secretVersions
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.map((sv) => ({
|
||||
type: "add",
|
||||
secretVersionId: sv.id
|
||||
}));
|
||||
|
||||
if (commitChanges.length > 0) {
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actorType || ActorType.PLATFORM,
|
||||
metadata: {
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Created",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await secretDAL.upsertSecretReferences(
|
||||
inputSecrets.map(({ references = [], key }) => ({
|
||||
secretId: newSecretGroupedByKeyName[key][0].id,
|
||||
@@ -185,6 +210,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
orgId,
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
folderCommitService,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
resourceMetadataDAL,
|
||||
@@ -259,6 +285,31 @@ export const fnSecretBulkUpdate = async ({
|
||||
),
|
||||
tx
|
||||
);
|
||||
|
||||
const commitChanges = secretVersions
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}));
|
||||
if (commitChanges.length > 0) {
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actorType || ActorType.PLATFORM,
|
||||
metadata: {
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Updated",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await secretDAL.upsertSecretReferences(
|
||||
inputSecrets
|
||||
.filter(({ data: { references } }) => Boolean(references))
|
||||
@@ -337,8 +388,11 @@ export const fnSecretBulkDelete = async ({
|
||||
inputSecrets,
|
||||
tx,
|
||||
actorId,
|
||||
actorType,
|
||||
secretDAL,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
folderCommitService,
|
||||
secretVersionDAL
|
||||
}: TFnSecretBulkDelete) => {
|
||||
const deletedSecrets = await secretDAL.deleteMany(
|
||||
inputSecrets.map(({ type, secretKey }) => ({
|
||||
@@ -358,6 +412,35 @@ export const fnSecretBulkDelete = async ({
|
||||
)
|
||||
);
|
||||
|
||||
const secretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||
folderId,
|
||||
deletedSecrets.map(({ id }) => id),
|
||||
tx
|
||||
);
|
||||
|
||||
const commitChanges = deletedSecrets
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.map(({ id }) => ({
|
||||
type: "delete",
|
||||
secretVersionId: secretVersions[id].id
|
||||
}));
|
||||
if (commitChanges.length > 0) {
|
||||
await folderCommitService.createCommit(
|
||||
{
|
||||
actor: {
|
||||
type: actorType || ActorType.PLATFORM,
|
||||
metadata: {
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Deleted",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return deletedSecrets;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -90,6 +91,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
@@ -124,6 +126,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectEnvDAL,
|
||||
secretTagDAL,
|
||||
secretVersionDAL,
|
||||
folderCommitService,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
snapshotService,
|
||||
@@ -327,6 +330,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
resourceMetadataDAL,
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
folderCommitService,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
@@ -510,6 +514,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderId,
|
||||
orgId: actorOrgId,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
inputSecrets: [
|
||||
{
|
||||
filter: { id: secretId },
|
||||
@@ -650,6 +655,9 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
actorType: actor,
|
||||
folderCommitService,
|
||||
secretVersionDAL,
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: [
|
||||
@@ -1590,6 +1598,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
orgId: actorOrgId,
|
||||
secretDAL,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
@@ -1859,6 +1868,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
|
||||
folderId,
|
||||
orgId: actorOrgId,
|
||||
folderCommitService,
|
||||
tx,
|
||||
inputSecrets: secretsToUpdate.map((el) => {
|
||||
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
|
||||
@@ -1928,6 +1938,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
folderCommitService,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
@@ -2061,6 +2072,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
fnSecretBulkDelete({
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
folderCommitService,
|
||||
secretVersionDAL,
|
||||
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
|
||||
secretKey,
|
||||
type: type || SecretType.Shared
|
||||
@@ -2068,6 +2081,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
actorType: actor,
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -2164,10 +2178,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
findOpt: {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
}
|
||||
});
|
||||
return secretVersions.map((el) => {
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
@@ -2469,6 +2487,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
tx,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
@@ -2495,6 +2514,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderId: destinationFolder.id,
|
||||
orgId: actorOrgId,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
secretVersionDAL,
|
||||
secretDAL,
|
||||
tx,
|
||||
@@ -2840,6 +2860,73 @@ export const secretV2BridgeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretVersionsByIds = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
secretVersionNumbers,
|
||||
folderId
|
||||
}: TGetSecretVersionsDTO & {
|
||||
secretVersionNumbers: string[];
|
||||
folderId: string;
|
||||
}) => {
|
||||
const folder = await folderDAL.findById(folderId);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${folderId}' not found` });
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
|
||||
|
||||
if (!folderWithPath) {
|
||||
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: folder.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
secretVersions: secretVersionNumbers
|
||||
});
|
||||
return secretVersions.map((el) => {
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: folder.environment.envSlug,
|
||||
secretPath: folderWithPath.path,
|
||||
secretName: el.key,
|
||||
...(el.tags?.length && {
|
||||
secretTags: el.tags.map((tag) => tag.slug)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
return reshapeBridgeSecret(
|
||||
folder.projectId,
|
||||
folder.environment.envSlug,
|
||||
folderWithPath.path,
|
||||
{
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@@ -2858,6 +2945,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById,
|
||||
getAccessibleSecrets
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
@@ -178,9 +179,10 @@ export type TFnSecretBulkInsert = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
actorId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -206,9 +208,10 @@ export type TFnSecretBulkUpdate = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
actorId?: string;
|
||||
};
|
||||
tx?: Knex;
|
||||
};
|
||||
@@ -218,11 +221,14 @@ export type TFnSecretBulkDelete = {
|
||||
projectId: string;
|
||||
inputSecrets: Array<{ type: SecretType; secretKey: string }>;
|
||||
actorId: string;
|
||||
actorType?: string;
|
||||
tx?: Knex;
|
||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
|
||||
secretQueueService: {
|
||||
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
|
||||
};
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "findLatestVersionMany">;
|
||||
};
|
||||
|
||||
export type THandleReminderDTO = {
|
||||
|
||||
@@ -138,7 +138,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
|
||||
throw new DatabaseError({ error, name: "FindLatestVersionMany" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,13 +174,21 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||
};
|
||||
|
||||
const findVersionsBySecretIdWithActors = async (
|
||||
secretId: string,
|
||||
projectId: string,
|
||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
const findVersionsBySecretIdWithActors = async ({
|
||||
secretId,
|
||||
projectId,
|
||||
secretVersions,
|
||||
findOpt = {},
|
||||
tx
|
||||
}: {
|
||||
secretId: string;
|
||||
projectId: string;
|
||||
secretVersions?: string[];
|
||||
findOpt?: TFindOpt<TSecretVersionsV2>;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
try {
|
||||
const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt;
|
||||
const query = (tx || db)(TableName.SecretVersionV2)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||
.leftJoin(
|
||||
@@ -203,10 +211,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.where((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretVersionV2),
|
||||
@@ -262,6 +272,84 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByIdsWithLatestVersion = async (
|
||||
folderId: string,
|
||||
secretIds: string[],
|
||||
versionIds?: string[],
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
if (!secretIds.length && (!versionIds || !versionIds.length)) return {};
|
||||
|
||||
const knexInstance = tx || db.replicaNode();
|
||||
let allDocs: Array<TSecretVersionsV2 & { max?: number }> = [];
|
||||
|
||||
// Process latest versions for secretIds
|
||||
if (secretIds.length) {
|
||||
const latestVersions: Array<TSecretVersionsV2 & { max: number }> = await knexInstance(TableName.SecretVersionV2)
|
||||
.where("folderId", folderId)
|
||||
.whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds)
|
||||
.join(
|
||||
knexInstance(TableName.SecretVersionV2)
|
||||
.groupBy("secretId")
|
||||
.max("version")
|
||||
.select("secretId")
|
||||
.as("latestVersion"),
|
||||
(bd) => {
|
||||
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
|
||||
`${TableName.SecretVersionV2}.version`,
|
||||
"latestVersion.max"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
allDocs = [...allDocs, ...latestVersions];
|
||||
}
|
||||
|
||||
// Process specific versions by versionIds
|
||||
if (versionIds && versionIds.length) {
|
||||
// Get the specific versions
|
||||
const specificVersions = await knexInstance(TableName.SecretVersionV2)
|
||||
.where("folderId", folderId)
|
||||
.whereIn("id", versionIds);
|
||||
|
||||
// Get the secretIds from these versions
|
||||
const specificSecretIds = [...new Set(specificVersions.map((v) => v.secretId).filter(Boolean))];
|
||||
|
||||
// Get max versions for these secretIds
|
||||
const maxVersionsQuery = (await knexInstance(TableName.SecretVersionV2)
|
||||
.whereIn("secretId", specificSecretIds)
|
||||
.groupBy("secretId")
|
||||
.select("secretId")
|
||||
.max("version as max")) as Array<{ secretId: string; max: number }>;
|
||||
|
||||
// Create a lookup map for max versions
|
||||
const maxVersionMap = maxVersionsQuery.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.secretId] = item.max;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Add max version to each specific version
|
||||
const specificVersionsWithMax = specificVersions.map((version) => ({
|
||||
...version,
|
||||
max: maxVersionMap[version.secretId]
|
||||
}));
|
||||
|
||||
allDocs = [...allDocs, ...specificVersionsWithMax];
|
||||
}
|
||||
|
||||
return allDocs.reduce<Record<string, TSecretVersionsV2 & { max?: number }>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretVersionV2Orm,
|
||||
pruneExcessVersions,
|
||||
@@ -269,6 +357,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
bulkUpdate,
|
||||
findLatestVersionByFolderId,
|
||||
findVersionsBySecretIdWithActors,
|
||||
findBySecretId
|
||||
findBySecretId,
|
||||
findByIdsWithLatestVersion
|
||||
};
|
||||
};
|
||||
|
||||
@@ -778,6 +778,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
folderCommitService,
|
||||
kmsService,
|
||||
resourceMetadataDAL
|
||||
}: TCreateManySecretsRawFnFactory) => {
|
||||
@@ -850,6 +851,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
folderCommitService,
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -942,6 +944,7 @@ export const updateManySecretsRawFnFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
kmsService
|
||||
}: TUpdateManySecretsRawFnFactory) => {
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||
@@ -1032,7 +1035,8 @@ export const updateManySecretsRawFnFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
folderCommitService
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-q
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||
@@ -112,6 +113,7 @@ type TSecretQueueFactoryDep = {
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
secretReminderRecipientsDAL: Pick<
|
||||
TSecretReminderRecipientsDALFactory,
|
||||
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
|
||||
@@ -178,7 +180,8 @@ export const secretQueueFactory = ({
|
||||
projectKeyDAL,
|
||||
resourceMetadataDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
secretSyncQueue
|
||||
secretSyncQueue,
|
||||
folderCommitService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||
@@ -366,7 +369,8 @@ export const secretQueueFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||
@@ -382,7 +386,8 @@ export const secretQueueFactory = ({
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -2521,6 +2521,36 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getSecretVersionsV2ByIds = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
secretVersions,
|
||||
folderId
|
||||
}: TGetSecretVersionsDTO & {
|
||||
secretVersions: string[];
|
||||
folderId: string;
|
||||
}) => {
|
||||
const secretVersionV2 = await secretV2BridgeService
|
||||
.getSecretVersionsByIds({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
folderId,
|
||||
secretVersionNumbers: secretVersions
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).message === "BadRequest: Failed to find secret") {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return secretVersionV2;
|
||||
};
|
||||
|
||||
const attachTags = async ({
|
||||
secretName,
|
||||
tagSlugs,
|
||||
@@ -3309,6 +3339,7 @@ export const secretServiceFactory = ({
|
||||
getSecretsRawByFolderMappings,
|
||||
getSecretAccessList,
|
||||
getSecretByIdRaw,
|
||||
getAccessibleSecrets
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsV2ByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||
@@ -441,6 +442,7 @@ export type TCreateManySecretsRawFnFactory = {
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
export type TCreateManySecretsRawFn = {
|
||||
@@ -478,6 +480,7 @@ export type TUpdateManySecretsRawFnFactory = {
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
};
|
||||
|
||||
export type TUpdateManySecretsRawFn = {
|
||||
|
||||
Reference in New Issue
Block a user