diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8032c17..ed21d28 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.0" + ".": "0.13.0" } diff --git a/.stats.yml b/.stats.yml index 71cbc3b..2abe9c2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-876b3cc61d59eee7f50b21239e174146afb6176473af1f7c6ac6ce8900e40b9c.yml -openapi_spec_hash: f0acdda9bda9e4733d298cf9082b58e7 -config_hash: 0b48961f5f0b4dca3a9a4342cd42bd51 +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-1ecd882ecd3b1995b6712bb140c7b9e6c6002963b311031d49146101af445dd1.yml +openapi_spec_hash: 529cbb1bf065a81cbf310160c330dbe6 +config_hash: a4efd21029f0fb9bf9e9abd69b06ac84 diff --git a/CHANGELOG.md b/CHANGELOG.md index b628da7..0c6072d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.13.0 (2026-08-19) + +Full Changelog: [v0.12.0...v0.13.0](https://github.com/formbricks/hub-typescript/compare/v0.12.0...v0.13.0) + +### Features + +* add a records-scoped tenant purge endpoint (ENG-2129) ([a6b9afa](https://github.com/formbricks/hub-typescript/commit/a6b9afa33356b7f582c11c0f56d6d242153ee0da)) + ## 0.12.0 (2026-08-12) Full Changelog: [v0.11.0...v0.12.0](https://github.com/formbricks/hub-typescript/compare/v0.11.0...v0.12.0) diff --git a/api.md b/api.md index ee5731a..ca8079d 100644 --- a/api.md +++ b/api.md @@ -61,10 +61,12 @@ Methods: Types: - TenantDeleteDataResponse +- TenantPurgeFeedbackRecordsResponse Methods: - client.tenants.deleteData(tenantID) -> TenantDeleteDataResponse +- client.tenants.purgeFeedbackRecords(tenantID) -> TenantPurgeFeedbackRecordsResponse ## Settings diff --git a/package.json b/package.json index fbccf89..f6b6352 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/hub", - "version": "0.12.0", + "version": "0.13.0", "description": "The official TypeScript library for the Formbricks Hub API", "author": "Formbricks Hub ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index ca84895..7cd9692 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,7 +1,7 @@ { "dxt_version": "0.2", "name": "@formbricks/hub-mcp", - "version": "0.12.0", + "version": "0.13.0", "description": "The official MCP Server for the Formbricks Hub API", "author": { "name": "Formbricks Hub", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index d55f4e1..dc14083 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/hub-mcp", - "version": "0.12.0", + "version": "0.13.0", "description": "The official MCP Server for the Formbricks Hub API", "author": "Formbricks Hub ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts index dcfd713..dab8829 100644 --- a/packages/mcp-server/src/code-tool-worker.ts +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -124,6 +124,7 @@ const fuse = new Fuse( 'client.webhooks.retrieve', 'client.webhooks.update', 'client.tenants.deleteData', + 'client.tenants.purgeFeedbackRecords', 'client.tenants.settings.retrieve', 'client.tenants.settings.update', 'client.taxonomy.listFields', diff --git a/packages/mcp-server/src/local-docs-search.ts b/packages/mcp-server/src/local-docs-search.ts index 39df01d..1d6753e 100644 --- a/packages/mcp-server/src/local-docs-search.ts +++ b/packages/mcp-server/src/local-docs-search.ts @@ -554,6 +554,31 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, }, }, + { + name: 'purge_feedback_records', + endpoint: '/v1/tenants/{tenant_id}/feedback-records', + httpMethod: 'delete', + summary: 'Purge all feedback records for a tenant', + description: + 'Permanently deletes every feedback record for the specified tenant_id, everything derived from\nthose records — embeddings and the enrichment stored on each record (sentiment, emotions,\ntranslations) — and the taxonomy built on them: runs, clusters, nodes, cluster memberships,\nactive-run pointers and node events.\n\nThis is intended for emptying a dataset that stays in use, so it removes the tenant\'s DATA but\nnever its CONFIGURATION: webhooks and tenant settings are left untouched. That is the difference\nfrom `DELETE /v1/tenants/{tenant_id}/data`, which additionally deletes both and is meant for\noffboarding a deprovisioned tenant.\n\nThe taxonomy is removed rather than preserved because it describes records that no longer exist:\nits per-node counts are derived from memberships and would all read zero, while a run\'s own stored\ncounters (`record_count`, `cluster_count`, a cluster\'s `size`) are historical and would keep\nadvertising the old numbers. A new taxonomy can be generated once the dataset has enough feedback\nagain; manual node renames and removals are per-run and do not survive a regeneration in any case.\n\nThe tenant is a required path segment, so it cannot be omitted the way a filter on a collection\ndelete could be — dropping it routes elsewhere rather than widening the operation to every tenant.\n\nAsynchronous: the request schedules the purge and returns 202 immediately, because the deletion is\nunbounded and can outlive a request. There is therefore no deleted count in the response. Poll\n`GET /v1/feedback-records/count?tenant_id=...` to observe progress.\n\nThe purge removes only the records that existed when it started — it takes a high-water mark up\nfront — so feedback ingested while it runs is never deleted. For a dataset that is no longer\nreceiving feedback the count reaching zero means the purge is complete; for one still ingesting,\na nonzero count is those newer records. Note this means the count alone cannot distinguish\n"finished" from "failed" on an active dataset.\n\nIdempotent and safe to repeat. Requesting a purge while one is already running for the same tenant\njoins the running purge rather than queueing a second one, and still returns 202. A purge requested\nafter an earlier one finished starts a new run.\n\nRecords are deleted in committed batches, so a purge interrupted by a restart or a timeout keeps\nthe progress it made and resumes on retry; the taxonomy is removed in a final step once the records\nare gone. While each step runs, the tenant\'s write lock is held exclusively and Hub-owned writes\nfor that tenant are rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released\nbetween steps, and writes for other tenants are never affected. This does not apply to the request below — scheduling a purge takes no lock, so\nthis endpoint does not return 409. A batch that cannot acquire the lock is retried by the job\nqueue without any caller action, up to a bounded number of attempts.\n\nNo webhook events are published for a purge, and no webhooks are deleted. Enrichment jobs already\nqueued for purged records no-op when they run, since the record they reference is gone.\n', + stainlessPath: '(resource) tenants > (method) purge_feedback_records', + qualified: 'client.tenants.purgeFeedbackRecords', + params: ['tenant_id: string;'], + response: "{ status: 'accepted'; tenant_id: string; message?: string; }", + markdown: + "## purge_feedback_records\n\n`client.tenants.purgeFeedbackRecords(tenant_id: string): { status: 'accepted'; tenant_id: string; message?: string; }`\n\n**delete** `/v1/tenants/{tenant_id}/feedback-records`\n\nPermanently deletes every feedback record for the specified tenant_id, everything derived from\nthose records — embeddings and the enrichment stored on each record (sentiment, emotions,\ntranslations) — and the taxonomy built on them: runs, clusters, nodes, cluster memberships,\nactive-run pointers and node events.\n\nThis is intended for emptying a dataset that stays in use, so it removes the tenant's DATA but\nnever its CONFIGURATION: webhooks and tenant settings are left untouched. That is the difference\nfrom `DELETE /v1/tenants/{tenant_id}/data`, which additionally deletes both and is meant for\noffboarding a deprovisioned tenant.\n\nThe taxonomy is removed rather than preserved because it describes records that no longer exist:\nits per-node counts are derived from memberships and would all read zero, while a run's own stored\ncounters (`record_count`, `cluster_count`, a cluster's `size`) are historical and would keep\nadvertising the old numbers. A new taxonomy can be generated once the dataset has enough feedback\nagain; manual node renames and removals are per-run and do not survive a regeneration in any case.\n\nThe tenant is a required path segment, so it cannot be omitted the way a filter on a collection\ndelete could be — dropping it routes elsewhere rather than widening the operation to every tenant.\n\nAsynchronous: the request schedules the purge and returns 202 immediately, because the deletion is\nunbounded and can outlive a request. There is therefore no deleted count in the response. Poll\n`GET /v1/feedback-records/count?tenant_id=...` to observe progress.\n\nThe purge removes only the records that existed when it started — it takes a high-water mark up\nfront — so feedback ingested while it runs is never deleted. For a dataset that is no longer\nreceiving feedback the count reaching zero means the purge is complete; for one still ingesting,\na nonzero count is those newer records. Note this means the count alone cannot distinguish\n\"finished\" from \"failed\" on an active dataset.\n\nIdempotent and safe to repeat. Requesting a purge while one is already running for the same tenant\njoins the running purge rather than queueing a second one, and still returns 202. A purge requested\nafter an earlier one finished starts a new run.\n\nRecords are deleted in committed batches, so a purge interrupted by a restart or a timeout keeps\nthe progress it made and resumes on retry; the taxonomy is removed in a final step once the records\nare gone. While each step runs, the tenant's write lock is held exclusively and Hub-owned writes\nfor that tenant are rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released\nbetween steps, and writes for other tenants are never affected. This does not apply to the request below — scheduling a purge takes no lock, so\nthis endpoint does not return 409. A batch that cannot acquire the lock is retried by the job\nqueue without any caller action, up to a bounded number of attempts.\n\nNo webhook events are published for a purge, and no webhooks are deleted. Enrichment jobs already\nqueued for purged records no-op when they run, since the record they reference is gone.\n\n\n### Parameters\n\n- `tenant_id: string`\n\n### Returns\n\n- `{ status: 'accepted'; tenant_id: string; message?: string; }`\n\n - `status: 'accepted'`\n - `tenant_id: string`\n - `message?: string`\n\n### Example\n\n```typescript\nimport FormbricksHub from '@formbricks/hub';\n\nconst client = new FormbricksHub();\n\nconst response = await client.tenants.purgeFeedbackRecords('org-123');\n\nconsole.log(response);\n```", + perLanguage: { + typescript: { + method: 'client.tenants.purgeFeedbackRecords', + example: + "import FormbricksHub from '@formbricks/hub';\n\nconst client = new FormbricksHub({\n apiKey: process.env['HUB_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.purgeFeedbackRecords('org-123');\n\nconsole.log(response.tenant_id);", + }, + http: { + example: + 'curl http://localhost:8080/v1/tenants/$TENANT_ID/feedback-records \\\n -X DELETE \\\n -H "Authorization: Bearer $HUB_API_KEY"', + }, + }, + }, { name: 'retrieve', endpoint: '/v1/tenants/{tenant_id}/settings', diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts index aa24431..3e955c3 100644 --- a/packages/mcp-server/src/methods.ts +++ b/packages/mcp-server/src/methods.ts @@ -106,6 +106,12 @@ export const sdkMethods: SdkMethod[] = [ httpMethod: 'delete', httpPath: '/v1/tenants/{tenant_id}/data', }, + { + clientCallName: 'client.tenants.purgeFeedbackRecords', + fullyQualifiedName: 'tenants.purgeFeedbackRecords', + httpMethod: 'delete', + httpPath: '/v1/tenants/{tenant_id}/feedback-records', + }, { clientCallName: 'client.tenants.settings.retrieve', fullyQualifiedName: 'tenants.settings.retrieve', diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 89b2343..786b706 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -28,7 +28,7 @@ export const newMcpServer = async ({ new McpServer( { name: 'formbricks_hub_api', - version: '0.12.0', + version: '0.13.0', }, { instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }), diff --git a/src/client.ts b/src/client.ts index 172e98a..dca4ff2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -55,7 +55,11 @@ import { TaxonomyListFieldsParams, TaxonomyListFieldsResponse, } from './resources/taxonomy/taxonomy'; -import { TenantDeleteDataResponse, Tenants } from './resources/tenants/tenants'; +import { + TenantDeleteDataResponse, + TenantPurgeFeedbackRecordsResponse, + Tenants, +} from './resources/tenants/tenants'; import { type Fetch } from './internal/builtin-types'; import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; import { FinalRequestOptions, RequestOptions } from './internal/request-options'; @@ -837,7 +841,11 @@ export declare namespace FormbricksHub { type WebhookListParams as WebhookListParams, }; - export { Tenants as Tenants, type TenantDeleteDataResponse as TenantDeleteDataResponse }; + export { + Tenants as Tenants, + type TenantDeleteDataResponse as TenantDeleteDataResponse, + type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse, + }; export { Taxonomy as Taxonomy, diff --git a/src/resources/index.ts b/src/resources/index.ts index fc2add8..7ace492 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -28,7 +28,11 @@ export { type TaxonomyListFieldsResponse, type TaxonomyListFieldsParams, } from './taxonomy/taxonomy'; -export { Tenants, type TenantDeleteDataResponse } from './tenants/tenants'; +export { + Tenants, + type TenantDeleteDataResponse, + type TenantPurgeFeedbackRecordsResponse, +} from './tenants/tenants'; export { Webhooks, type WebhookCreateResponse, diff --git a/src/resources/tenants/index.ts b/src/resources/tenants/index.ts index ab54cc0..65d3160 100644 --- a/src/resources/tenants/index.ts +++ b/src/resources/tenants/index.ts @@ -6,4 +6,4 @@ export { type SettingUpdateResponse, type SettingUpdateParams, } from './settings'; -export { Tenants, type TenantDeleteDataResponse } from './tenants'; +export { Tenants, type TenantDeleteDataResponse, type TenantPurgeFeedbackRecordsResponse } from './tenants'; diff --git a/src/resources/tenants/tenants.ts b/src/resources/tenants/tenants.ts index d6b3698..bd72b0a 100644 --- a/src/resources/tenants/tenants.ts +++ b/src/resources/tenants/tenants.ts @@ -41,6 +41,76 @@ export class Tenants extends APIResource { deleteData(tenantID: string, options?: RequestOptions): APIPromise { return this._client.delete(path`/v1/tenants/${tenantID}/data`, options); } + + /** + * Permanently deletes every feedback record for the specified tenant_id, + * everything derived from those records — embeddings and the enrichment stored on + * each record (sentiment, emotions, translations) — and the taxonomy built on + * them: runs, clusters, nodes, cluster memberships, active-run pointers and node + * events. + * + * This is intended for emptying a dataset that stays in use, so it removes the + * tenant's DATA but never its CONFIGURATION: webhooks and tenant settings are left + * untouched. That is the difference from `DELETE /v1/tenants/{tenant_id}/data`, + * which additionally deletes both and is meant for offboarding a deprovisioned + * tenant. + * + * The taxonomy is removed rather than preserved because it describes records that + * no longer exist: its per-node counts are derived from memberships and would all + * read zero, while a run's own stored counters (`record_count`, `cluster_count`, a + * cluster's `size`) are historical and would keep advertising the old numbers. A + * new taxonomy can be generated once the dataset has enough feedback again; manual + * node renames and removals are per-run and do not survive a regeneration in any + * case. + * + * The tenant is a required path segment, so it cannot be omitted the way a filter + * on a collection delete could be — dropping it routes elsewhere rather than + * widening the operation to every tenant. + * + * Asynchronous: the request schedules the purge and returns 202 immediately, + * because the deletion is unbounded and can outlive a request. There is therefore + * no deleted count in the response. Poll + * `GET /v1/feedback-records/count?tenant_id=...` to observe progress. + * + * The purge removes only the records that existed when it started — it takes a + * high-water mark up front — so feedback ingested while it runs is never deleted. + * For a dataset that is no longer receiving feedback the count reaching zero means + * the purge is complete; for one still ingesting, a nonzero count is those newer + * records. Note this means the count alone cannot distinguish "finished" from + * "failed" on an active dataset. + * + * Idempotent and safe to repeat. Requesting a purge while one is already running + * for the same tenant joins the running purge rather than queueing a second one, + * and still returns 202. A purge requested after an earlier one finished starts a + * new run. + * + * Records are deleted in committed batches, so a purge interrupted by a restart or + * a timeout keeps the progress it made and resumes on retry; the taxonomy is + * removed in a final step once the records are gone. While each step runs, the + * tenant's write lock is held exclusively and Hub-owned writes for that tenant are + * rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released + * between steps, and writes for other tenants are never affected. This does not + * apply to the request below — scheduling a purge takes no lock, so this endpoint + * does not return 409. A batch that cannot acquire the lock is retried by the job + * queue without any caller action, up to a bounded number of attempts. + * + * No webhook events are published for a purge, and no webhooks are deleted. + * Enrichment jobs already queued for purged records no-op when they run, since the + * record they reference is gone. + * + * @example + * ```ts + * const response = await client.tenants.purgeFeedbackRecords( + * 'org-123', + * ); + * ``` + */ + purgeFeedbackRecords( + tenantID: string, + options?: RequestOptions, + ): APIPromise { + return this._client.delete(path`/v1/tenants/${tenantID}/feedback-records`, options); + } } export interface TenantDeleteDataResponse { @@ -100,10 +170,32 @@ export interface TenantDeleteDataResponse { tenant_id: string; } +export interface TenantPurgeFeedbackRecordsResponse { + /** + * Always `accepted`. The purge runs in the background, so this reports that the + * work was scheduled, not that it finished — poll `GET /v1/feedback-records/count` + * for the tenant to observe completion. + */ + status: 'accepted'; + + /** + * Tenant ID whose feedback records are being purged + */ + tenant_id: string; + + /** + * Human-readable confirmation + */ + message?: string; +} + Tenants.Settings = Settings; export declare namespace Tenants { - export { type TenantDeleteDataResponse as TenantDeleteDataResponse }; + export { + type TenantDeleteDataResponse as TenantDeleteDataResponse, + type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse, + }; export { Settings as Settings, diff --git a/src/version.ts b/src/version.ts index ce6b899..9d013cc 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.12.0'; // x-release-please-version +export const VERSION = '0.13.0'; // x-release-please-version diff --git a/tests/api-resources/tenants/tenants.test.ts b/tests/api-resources/tenants/tenants.test.ts index 1c48fc6..5447b1e 100644 --- a/tests/api-resources/tenants/tenants.test.ts +++ b/tests/api-resources/tenants/tenants.test.ts @@ -19,4 +19,16 @@ describe('resource tenants', () => { expect(dataAndResponse.data).toBe(response); expect(dataAndResponse.response).toBe(rawResponse); }); + + // Mock server tests are disabled + test.skip('purgeFeedbackRecords', async () => { + const responsePromise = client.tenants.purgeFeedbackRecords('org-123'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); });