mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-21 11:49:32 -05:00
fix: switch Hub purge to DELETE /v1/tenants/{tenant_id}/data
Hub#77 ships the proper tenant-data purge endpoint that nukes feedback records, derived embeddings, and webhooks in one idempotent call. Swap the placeholder bulkDelete cast for a direct client.delete<>() against the new path and surface the per-resource counts. Renames deleteFeedbackRecordsByTenant → deleteHubTenantData to reflect that it deletes more than feedback records. Org-deletion call site and tests updated accordingly. Best-effort error handling unchanged.
This commit is contained in:
@@ -47,7 +47,10 @@ vi.mock("@/modules/ee/billing/lib/organization-billing", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/hub/service", () => ({
|
||||
deleteFeedbackRecordsByTenant: vi.fn().mockResolvedValue({ data: { deletedCount: 0 }, error: null }),
|
||||
deleteHubTenantData: vi.fn().mockResolvedValue({
|
||||
data: { deletedFeedbackRecords: 0, deletedEmbeddings: 0, deletedWebhooks: 0 },
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Organization Service", () => {
|
||||
@@ -369,8 +372,8 @@ describe("Organization Service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("should purge Hub feedback records for each feedback directory", async () => {
|
||||
const { deleteFeedbackRecordsByTenant } = await import("@/modules/hub/service");
|
||||
test("should purge Hub-owned data for each feedback directory", async () => {
|
||||
const { deleteHubTenantData } = await import("@/modules/hub/service");
|
||||
vi.mocked(prisma.organization.delete).mockResolvedValue({
|
||||
id: "org1",
|
||||
name: "Test Org",
|
||||
@@ -382,9 +385,9 @@ describe("Organization Service", () => {
|
||||
|
||||
await deleteOrganization("org1");
|
||||
|
||||
expect(deleteFeedbackRecordsByTenant).toHaveBeenCalledTimes(2);
|
||||
expect(deleteFeedbackRecordsByTenant).toHaveBeenCalledWith("frd_1");
|
||||
expect(deleteFeedbackRecordsByTenant).toHaveBeenCalledWith("frd_2");
|
||||
expect(deleteHubTenantData).toHaveBeenCalledTimes(2);
|
||||
expect(deleteHubTenantData).toHaveBeenCalledWith("frd_1");
|
||||
expect(deleteHubTenantData).toHaveBeenCalledWith("frd_2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import { updateUser } from "@/lib/user/service";
|
||||
import { getBillingUsageCycleWindow } from "@/lib/utils/billing";
|
||||
import { getWorkspaces } from "@/lib/workspace/service";
|
||||
import { cleanupStripeCustomer } from "@/modules/ee/billing/lib/organization-billing";
|
||||
import { deleteFeedbackRecordsByTenant } from "@/modules/hub/service";
|
||||
import { deleteHubTenantData } from "@/modules/hub/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
export const select = {
|
||||
@@ -308,10 +308,11 @@ export const deleteOrganization = async (organizationId: string) => {
|
||||
await cleanupStripeCustomer(stripeCustomerId);
|
||||
}
|
||||
|
||||
// Best-effort: purge feedback records in the Hub for each directory tenant.
|
||||
// Failures are logged inside the gateway and do not roll back the local delete.
|
||||
// Best-effort: purge Hub-owned data (feedback records, embeddings, webhooks) for each
|
||||
// directory tenant. Failures are logged inside the gateway and do not roll back the
|
||||
// local delete.
|
||||
for (const directory of deletedOrganization.feedbackDirectories) {
|
||||
await deleteFeedbackRecordsByTenant(directory.id);
|
||||
await deleteHubTenantData(directory.id);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -129,38 +129,51 @@ export const deleteFeedbackRecord = async (id: string): Promise<HubFeedbackRecor
|
||||
}
|
||||
};
|
||||
|
||||
export type HubFeedbackRecordsByTenantDeleteResult = {
|
||||
data: { deletedCount: number } | null;
|
||||
export type HubTenantDataDeleteResult = {
|
||||
data: {
|
||||
deletedFeedbackRecords: number;
|
||||
deletedEmbeddings: number;
|
||||
deletedWebhooks: number;
|
||||
} | null;
|
||||
error: HubError | null;
|
||||
};
|
||||
|
||||
type TenantDataDeleteResponse = {
|
||||
tenant_id: string;
|
||||
deleted_feedback_records: number;
|
||||
deleted_embeddings: number;
|
||||
deleted_webhooks: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all feedback records in the Hub for a given tenant.
|
||||
* Used when an organization (and its feedback directories) is deleted, so that
|
||||
* Hub-side records do not become orphaned.
|
||||
* Purge all Hub-owned data (feedback records, derived embeddings, webhooks) for a tenant.
|
||||
* Called when the owning organization is deleted so Hub-side rows don't become orphaned.
|
||||
* Idempotent on the Hub side; the caller treats failures as best-effort.
|
||||
*
|
||||
* NOTE: depends on the Hub `bulkDelete` endpoint accepting a `tenant_id`-only
|
||||
* payload (no `user_id`). Until that ships, this call will fail with a 4xx and
|
||||
* be logged as a warning — caller treats this as best-effort.
|
||||
* Hits `DELETE /v1/tenants/{tenant_id}/data` directly because the SDK doesn't yet expose
|
||||
* a typed method for this endpoint.
|
||||
*/
|
||||
export const deleteFeedbackRecordsByTenant = async (
|
||||
tenantId: string
|
||||
): Promise<HubFeedbackRecordsByTenantDeleteResult> => {
|
||||
export const deleteHubTenantData = async (tenantId: string): Promise<HubTenantDataDeleteResult> => {
|
||||
const client = getHubClient();
|
||||
if (!client) {
|
||||
return { data: null, error: { ...NO_CONFIG_ERROR } };
|
||||
}
|
||||
|
||||
try {
|
||||
// Cast: SDK currently requires `user_id`. Hub-side change will accept a
|
||||
// tenant-only payload; until the SDK types catch up we go through `unknown`.
|
||||
const bulkDelete = client.feedbackRecords.bulkDelete as unknown as (params: {
|
||||
tenant_id: string;
|
||||
}) => Promise<{ deleted_count: number }>;
|
||||
const data = await bulkDelete({ tenant_id: tenantId });
|
||||
return { data: { deletedCount: data.deleted_count }, error: null };
|
||||
const data = await client.delete<TenantDataDeleteResponse>(
|
||||
`/v1/tenants/${encodeURIComponent(tenantId)}/data`
|
||||
);
|
||||
return {
|
||||
data: {
|
||||
deletedFeedbackRecords: data.deleted_feedback_records,
|
||||
deletedEmbeddings: data.deleted_embeddings,
|
||||
deletedWebhooks: data.deleted_webhooks,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.warn({ err, tenantId }, "Hub: deleteFeedbackRecordsByTenant failed");
|
||||
logger.warn({ err, tenantId }, "Hub: deleteHubTenantData failed");
|
||||
const status = getErrorStatus(err);
|
||||
const message = getErrorMessage(err);
|
||||
return { data: null, error: { status, message, detail: message } };
|
||||
|
||||
Reference in New Issue
Block a user