feat(contacts): cover full public API for MCP, move logic into business services - #1093
Merged
Conversation
This was referenced Sep 6, 2026
Merged
…ss services Adds the remaining contacts public-API surface (bulk ops, export, refresh profile, notes/sequences/inboxes/filter-fields as their own features) so MCP and other workspace-token callers can fully manage contacts. In the process, moves business logic that lived only in server-action inner functions (create/update/delete contact, tag attach/detach/replace, custom-field apply, message send, contact import) into packages/business services and packages/database repositories, so the public API and the action call the same code path instead of duplicating it — fixing a refresh-profile 401 caused by a stray session check and a stale contact-notes cache that never invalidated on write. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…invalidation, close public API gaps Closes the review findings on the contacts public API/MCP surface: a workspace-A token could enroll contacts into a workspace-B sequence (sequential bigint ids, never validated); tag writes invalidated cache tags nothing ever reads, so renamed/deleted tags and stale contact tag lists persisted for up to 24h; and the custom-field update path silently dropped writes to fields 51+ in workspaces with more than 50 custom fields. Also makes bulk endpoints report skipped ids instead of a blind 204, surfaces channel-send enqueue failures instead of swallowing them, restores the sequential (not concurrent) pre-auth rate-limit gate, adds read caching now that invalidation is correct, and removes dead code left behind by the original PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ayering Audits every change on this branch against the action|API handler -> service -> repository -> DB model and fixes deviations: contacts list/count and messages queries move into contactService/messageService with repositories reduced to raw where-builders; public and private handlers converge on the same service methods (contactService.list, resolveContactInboxForConversation, contactCustomFieldService.applyOperations); trigger/webhook/flow create actions and the worker's contact.ts flow-step handlers drop direct `db` use in favor of triggerService.create, webhookService.create, flowService.createDraft, tagService.attachByNamesToContacts/detachByNamesFromContacts, and contactSequenceService.enrollFromFlow. Updates .agents/rules/data-access.md, 8 skills, AGENTS.md, CLAUDE.md, and workspace-api-tokens.md to document the layering contract, and adds business-layer test coverage for the new/moved service methods. subscribeBroadcast/unsubscribeBroadcast intentionally stay on direct db writes (a service swap would have dropped an idempotency guard and an unsubscribe event) - follow-up, not an oversight. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Umbodvf34JwbqNQsSGt7ZR
…ness barrel
media-library-trigger.tsx ("use client") value-imported
MEDIA_LIBRARY_FILES_PAGE_SIZE re-exported from @chatbotx.io/business,
whose 103-line barrel reaches the Postgres pool and Redis client -
dragging server-only deps into the browser bundle.
- Duplicate the constant into a feature-local constants.ts instead of
re-exporting it from the backend barrel.
- Add package.json subpath exports (./inbox/schema,
./ads-conversion/schema) for the three other schema files that
value-imported the same barrel for shared zod schemas, so those
keep a single source of truth without pulling in the barrel.
- Add an AST-based guard test that fails if any "use client" file
reaches @chatbotx.io/business (even transitively through first-party
re-exports), mirroring the existing client-imports-no-queries guard.
… for public API Moves remaining contact, tag, note, and import logic into packages/business services, tightens public API schemas, and removes dead trigger/webhook constants superseded by the worker-config schedule constants module. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VWhmQGALFoH4NVXMsXypA
realcodesiman
force-pushed
the
feat/contacts-public-api-mcp
branch
from
September 7, 2026 22:57
b4a6220 to
b463729
Compare
…yword text Four correctness fixes surfaced while finishing the contacts public API: - `contactService.list`/`count` now take a required `scope`. The public API must pass the explicit `UNSCOPED` literal instead of omitting the argument, so member scoping (and PII masking) can never be lost by a forgotten parameter on a PII-bearing read. - `automatedResponseService.create` throws when both `flowId` and `text` are given instead of silently nulling `text`. Template install forwards the manifest's fields verbatim, so the old behaviour dropped authored content with no error surfaced. - The oRPC error interceptor no longer emits a bare i18n key for a `ChatbotXException` carrying `data`; it appends the params so an API consumer gets a self-describing message. - `deleteContactNoteAction` swallows `notFound` so a double-click on the optimistic notes list does not raise an error toast. The public API keeps its 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VWhmQGALFoH4NVXMsXypA
realcodesiman
added a commit
that referenced
this pull request
Sep 8, 2026
…oks data access into business Removes direct db usage from flows, triggers, sequences, broadcasts, webhooks, the template resource picker and saved replies per .agents/rules/data-access.md. Public oRPC router keys, paths and the public-spec snapshot are unchanged. - new flow, sequence, broadcast, trigger, condition and template-selectable-resource repositories; webhook repository gains paginated list + detail reads - flowService.createWithDefaultDraft, flowVersionService.publish, triggerService/webhookService create/update/updateSettings/deleteMany (trigger and webhook condition-diff semantics kept separate), new sequenceService, broadcastService.create/resend, savedReplyService.listByWorkspaceId - broadcastService.create validates each integration id independently so the validation error lands on the field that failed - validationException added to errors.ts in the same form as #1093 - deleteSequence now also scopes by workspaceId (defense in depth)
realcodesiman
added a commit
that referenced
this pull request
Sep 8, 2026
…oks data access into business Removes direct db usage from flows, triggers, sequences, broadcasts, webhooks, the template resource picker and saved replies per .agents/rules/data-access.md. Public oRPC router keys, paths and the public-spec snapshot are unchanged. - new flow, sequence, broadcast, trigger, condition and template-selectable-resource repositories; webhook repository gains paginated list + detail reads - flowService.createWithDefaultDraft, flowVersionService.publish, triggerService/webhookService create/update/updateSettings/deleteMany (trigger and webhook condition-diff semantics kept separate), new sequenceService, broadcastService.create/resend, savedReplyService.listByWorkspaceId - broadcastService.create validates each integration id independently so the validation error lands on the field that failed - validationException added to errors.ts in the same form as #1093 - deleteSequence now also scopes by workspaceId (defense in depth)
realcodesiman
added a commit
that referenced
this pull request
Sep 8, 2026
…oks data access into business Removes direct db usage from flows, triggers, sequences, broadcasts, webhooks, the template resource picker and saved replies per .agents/rules/data-access.md. Public oRPC router keys, paths and the public-spec snapshot are unchanged. - new flow, sequence, broadcast, trigger, condition and template-selectable-resource repositories; webhook repository gains paginated list + detail reads - flowService.createWithDefaultDraft, flowVersionService.publish, triggerService/webhookService create/update/updateSettings/deleteMany (trigger and webhook condition-diff semantics kept separate), new sequenceService, broadcastService.create/resend, savedReplyService.listByWorkspaceId - broadcastService.create validates each integration id independently so the validation error lands on the field that failed - validationException added to errors.ts in the same form as #1093 - deleteSequence now also scopes by workspaceId (defense in depth)
realcodesiman
added a commit
that referenced
this pull request
Sep 9, 2026
…oks data access into business Removes direct db usage from flows, triggers, sequences, broadcasts, webhooks, the template resource picker and saved replies per .agents/rules/data-access.md. Public oRPC router keys, paths and the public-spec snapshot are unchanged. - new flow, sequence, broadcast, trigger, condition and template-selectable-resource repositories; webhook repository gains paginated list + detail reads - flowService.createWithDefaultDraft, flowVersionService.publish, triggerService/webhookService create/update/updateSettings/deleteMany (trigger and webhook condition-diff semantics kept separate), new sequenceService, broadcastService.create/resend, savedReplyService.listByWorkspaceId - broadcastService.create validates each integration id independently so the validation error lands on the field that failed - validationException added to errors.ts in the same form as #1093 - deleteSequence now also scopes by workspaceId (defense in depth)
realcodesiman
added a commit
that referenced
this pull request
Sep 9, 2026
… ai-agents, reflinks, ai-triggers (#1102) * refactor(builder): move flows, triggers, sequences, broadcasts, webhooks data access into business Removes direct db usage from flows, triggers, sequences, broadcasts, webhooks, the template resource picker and saved replies per .agents/rules/data-access.md. Public oRPC router keys, paths and the public-spec snapshot are unchanged. - new flow, sequence, broadcast, trigger, condition and template-selectable-resource repositories; webhook repository gains paginated list + detail reads - flowService.createWithDefaultDraft, flowVersionService.publish, triggerService/webhookService create/update/updateSettings/deleteMany (trigger and webhook condition-diff semantics kept separate), new sequenceService, broadcastService.create/resend, savedReplyService.listByWorkspaceId - broadcastService.create validates each integration id independently so the validation error lands on the field that failed - validationException added to errors.ts in the same form as #1093 - deleteSequence now also scopes by workspaceId (defense in depth) * feat(automation): widen public API to full CRUD across flows, triggers, keywords, ai-agents, reflinks, ai-triggers Finishes the data-access chain (action -> service -> repository -> DB) for flows, sequences, broadcasts, saved-replies, reflinks, bot-fields, and greenfield ai-triggers, then widens the `automation` token scope from read-only to full CRUD so MCP/agent clients can build, publish, and inspect automations end to end. * fix(automation): close data-loss and correctness bugs in PR #1102's public API Remediates review findings from PR #1102's data-access refactor and public API widening: - PUT /v1/keywords/{id} without `keywords` silently wiped the automation's keywords to `[]`; the service now leaves the column untouched when the caller omits it, and moves the text/flowId mutual-exclusion + cross- workspace flowId validation down from the action into the service so every caller gets the same invariants. - POST /v1/ai-agents could return the wrong resource on a duplicate name (no unique constraint); `create` now returns the inserted id and the handler re-fetches by id instead of by name. - public-spec-operations.test.ts had no explicit `beforeAll` timeout, causing CI-only flakiness as the OpenAPI snapshot grew. - GET /v1/triggers loaded every trigger in the workspace then re-queried each one individually; added `triggerService.list` backed by a single SQL-paginated query with conditions joined in. - Reflinks' `findReflink` swallowed every error (including infra failures) as not-found; added a nullable `reflinkService.find()`. - Public flow imports attributed every import to the workspace owner; now pass `userId: null`, matching the contacts public-API precedent. - `triggerResource` published `z.array(z.any())` for conditions; conditions now has a real, documented shape. - `resendBroadcast` read a broadcast's contact filter before verifying it was resendable, and dropped the `deletedAt` predicate; added `broadcastService.assertResendable` to guard first. - `updateSequenceAction` masked a 404 as a generic 500 by wrapping the whole call in a catch-all; now rethrows `ChatbotXException` unchanged. - `aiTriggerService.list` divided pageCount by an unclamped `perPage`. Also: dedicated tests for `webhookService.updateWithConditions`, `sequence/step-payload.ts`'s defaulting logic, `broadcastService.create`'s insert shape, and bot-field unique-violation mapping; replaced two hand-rolled `UNIQUE_VIOLATION_CODE` checks with `isUniqueViolationError`; collapsed `templateSelectableResourceRepository`'s 11 near-identical list methods into one generic helper; removed dead double-mapping of trigger/ webhook conditions now that the service owns column normalization. * fix(automation): restore flows list contract and field-level validation errors - GET /v1/flows again defaults `active: true` and returns `{id, name}` instead of the full flow row plus embedded flowVersions, avoiding a breaking change to an existing public-API consumer contract. - Add isValidationException (a real instanceof ChatbotXException guard) and use it in place of duck-typed `"code" in error` checks in reflinks, bot-fields, and sequences actions, which could mis-narrow on unrelated driver/system errors and always hardcoded the error message. - Wrap automatedResponseService.update in update-automated-response-action so the flowId-not-found validation exception (moved into the service by the prior refactor) still surfaces as a field-level form error instead of a generic toast. - Fix aiTriggerService.list to divide pageCount by the same clamped limit the repository uses (getPaginationWithDefaults), instead of a Math.min(maxLimit, perPage) expression that could divide by undefined. * fix(automation): scope keyword writes by type and stabilize public API pagination Third round of correctness fixes on the public API surface: - Keywords endpoints now pass `type: "inbound"` through findOrFail, update, setStatus and deleteMany so an outbound (Page) automated response can no longer be read, mutated or deleted through the inbound Keywords routes — one table serves two FolderTypes, so workspaceId + id alone is not a sufficient scope. - Add `flowVersionService.updateDraftByFlowId` for callers that only know the flow id; `PUT /v1/flows/{id}/draft` was passing a flow id where a flow-version id was expected. - Give `triggerRepository.listPaginatedWithConditions` a deterministic `orderBy` so paginated results cannot repeat or skip rows. - Skip no-op updates in ai-agent, ai-trigger and reflink services so an all-undefined payload no longer issues an empty SET or a spurious audit entry. - Write `questions: []` explicitly on ai-trigger create — the column has no database default despite the drizzle `.default()`. - Correct `flowService.list`'s return type to `limit`/`offset`, matching what `parsePagination` actually spreads. * fix(automation): unify trigger/webhook list pagination and drop redundant broadcast query Closes the remaining data-access gaps from the flows/triggers/sequences/ broadcasts/webhooks refactor: the public API and builder pages for triggers and webhooks each maintained a separate, diverging list implementation, and the webhook public API loaded every row in the workspace before paginating in memory. - webhookService.list: SQL-paginated, conditions joined, shared by GET /v1/webhooks and the builder's webhooks page (replaces the unbounded listByWorkspaceId + paginateInMemory path) - triggerService.list: extended with folderId/name filters so the builder's triggers page can share it with GET /v1/triggers instead of hand-rolling a second, unordered pagination; added triggerService.findWithConditions to remove three direct repository reads from the public API - resend-broadcast.action: read contactFilter off assertResendable's already-fetched row instead of issuing a second query for it - create-broadcast.action: use isValidationException instead of a duck-typed error check, matching every sibling action - flow detail pages: call flowService.findById instead of flowRepository directly * fix(automation): move audience/list reads and step scheduling into business services Closes remaining direct-repository access from the builder app layer for broadcasts (list, audience, findByIdOrName), sequences (list, findWithSteps), and moves sequence-step contact-schedule recalculation into packages/business/src/sequence, alongside triggers/ai-agents write handlers now returning their created/updated model instead of a redundant follow-up findBy. Automated-response reads/writes are scoped by type end-to-end (inbound vs outbound) to prevent cross-type leaks. * fix(automation): move template picker query into business and scope reflink writes Move the template picker's selectable-resource dispatch out of the builder query layer into `templateService.listSelectableResources`, so the query file is a thin adapter and the repository is no longer reached from `apps/`. Scope `reflinkService` update/deleteMany/listOptions by `type = "refLink"` to match what `create` stamps, so an entry-point-link row sharing the table can never be updated or deleted through the reflink surface. Stop swallowing every error on the flow pages: only a service-thrown `notFoundException` becomes `notFound()`, and a DB failure propagates as a real 500 instead of a misleading 404. Drop the now-unused `broadcastRepository.findIdIfActive` and `findContactFilter` along with their tests and service mocks.
cursor Bot
pushed a commit
to JuanBifrost/ChatbotX
that referenced
this pull request
Sep 10, 2026
…ss services (ChatbotXIO#1093) * feat(contacts): cover full public API for MCP, move logic into business services Adds the remaining contacts public-API surface (bulk ops, export, refresh profile, notes/sequences/inboxes/filter-fields as their own features) so MCP and other workspace-token callers can fully manage contacts. In the process, moves business logic that lived only in server-action inner functions (create/update/delete contact, tag attach/detach/replace, custom-field apply, message send, contact import) into packages/business services and packages/database repositories, so the public API and the action call the same code path instead of duplicating it — fixing a refresh-profile 401 caused by a stray session check and a stale contact-notes cache that never invalidated on write. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(contacts): scope sequence enrollment to workspace, fix tag cache invalidation, close public API gaps Closes the review findings on the contacts public API/MCP surface: a workspace-A token could enroll contacts into a workspace-B sequence (sequential bigint ids, never validated); tag writes invalidated cache tags nothing ever reads, so renamed/deleted tags and stale contact tag lists persisted for up to 24h; and the custom-field update path silently dropped writes to fields 51+ in workspaces with more than 50 custom fields. Also makes bulk endpoints report skipped ids instead of a blind 204, surfaces channel-send enqueue failures instead of swallowing them, restores the sequential (not concurrent) pre-auth rate-limit gate, adds read caching now that invalidation is correct, and removes dead code left behind by the original PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(contacts): align public API branch with service/repository layering Audits every change on this branch against the action|API handler -> service -> repository -> DB model and fixes deviations: contacts list/count and messages queries move into contactService/messageService with repositories reduced to raw where-builders; public and private handlers converge on the same service methods (contactService.list, resolveContactInboxForConversation, contactCustomFieldService.applyOperations); trigger/webhook/flow create actions and the worker's contact.ts flow-step handlers drop direct `db` use in favor of triggerService.create, webhookService.create, flowService.createDraft, tagService.attachByNamesToContacts/detachByNamesFromContacts, and contactSequenceService.enrollFromFlow. Updates .agents/rules/data-access.md, 8 skills, AGENTS.md, CLAUDE.md, and workspace-api-tokens.md to document the layering contract, and adds business-layer test coverage for the new/moved service methods. subscribeBroadcast/unsubscribeBroadcast intentionally stay on direct db writes (a service swap would have dropped an idempotency guard and an unsubscribe event) - follow-up, not an oversight. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Umbodvf34JwbqNQsSGt7ZR * fix(media-library): stop client bundle from pulling backend-only business barrel media-library-trigger.tsx ("use client") value-imported MEDIA_LIBRARY_FILES_PAGE_SIZE re-exported from @chatbotx.io/business, whose 103-line barrel reaches the Postgres pool and Redis client - dragging server-only deps into the browser bundle. - Duplicate the constant into a feature-local constants.ts instead of re-exporting it from the backend barrel. - Add package.json subpath exports (./inbox/schema, ./ads-conversion/schema) for the three other schema files that value-imported the same barrel for shared zod schemas, so those keep a single source of truth without pulling in the barrel. - Add an AST-based guard test that fails if any "use client" file reaches @chatbotx.io/business (even transitively through first-party re-exports), mirroring the existing client-imports-no-queries guard. * refactor(contacts): finish service-layer alignment and schema cleanup for public API Moves remaining contact, tag, note, and import logic into packages/business services, tightens public API schemas, and removes dead trigger/webhook constants superseded by the worker-config schedule constants module. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VWhmQGALFoH4NVXMsXypA * fix(contacts): require explicit UNSCOPED opt-out and stop dropping keyword text Four correctness fixes surfaced while finishing the contacts public API: - `contactService.list`/`count` now take a required `scope`. The public API must pass the explicit `UNSCOPED` literal instead of omitting the argument, so member scoping (and PII masking) can never be lost by a forgotten parameter on a PII-bearing read. - `automatedResponseService.create` throws when both `flowId` and `text` are given instead of silently nulling `text`. Template install forwards the manifest's fields verbatim, so the old behaviour dropped authored content with no error surfaced. - The oRPC error interceptor no longer emits a bare i18n key for a `ChatbotXException` carrying `data`; it appends the params so an API consumer gets a self-describing message. - `deleteContactNoteAction` swallows `notFound` so a double-click on the optimistic notes list does not raise an error toast. The public API keeps its 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VWhmQGALFoH4NVXMsXypA --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/businessservices andpackages/databaserepositories, so the public API handler and the private server action call the exact same code path instead of diverging copies.refresh-profilereturned 401 for token callers because the old code path required a browser session;contacts_createNote→contacts_listNotesin the same MCP session could see a stale note because nothing invalidated the notes cache on write.Changes
contact-notes,contact-sequences,contact-inboxes,contact-filter(each with its ownapi/public.ts+schema/public.ts), composed intofeatures/contacts/api/public.ts; pluscontacts/api/public/{bulk,export,refresh-profile,tags,custom-fields}.ts.contact/create-with-inbox.ts,contact/update-fields.ts,contact-export/service.ts,message/create-outgoing.ts,database/repositories/contact/,database/repositories/media-library-file/.create-contact,delete-contact,add/remove/update-contact-tag,add-contact-custom-field,update-contact-field,create-message,create-tag,delete-tag,import-contacts,create-webhook,create-flow,create-trigger,create-automated-response) now delegate to the shared business service instead of holding their own copy of the logic.features/contacts/server/**(legacy ad-hoc business layer),queries/public-find-contact.ts,folders/actions/utils.ts, unused tag store provider files.packages/database/drizzle/20260905151444_...)..agents/skills/{orpc-api,feature-scaffold}/SKILL.md(submodule composition pattern,server/directory ban),docs/developer/workspace-api-tokens.md(accurate endpoint-to-scope table and test list).Follow-up: layering audit (latest commit)
Audited every change on this branch against the
action | API handler → service → repository → DBmodel and closed the remaining gaps:contactService/messageService, with repositories reduced to raw where-builders (list-contacts.queries.tsand the messages query adapter both shrink to thin request adapters).contactService.list,conversationService.resolveContactInboxForConversation,contactCustomFieldService.applyOperations.triggerService.create,webhookService.create, andflowService.createDraftreplace directdbuse in the corresponding create actions.apps/worker/src/integration/handlers/contact.tsflow-step handlers now delegate tocontactService.update,contactNoteService.create,tagService.attachByNamesToContacts/detachByNamesFromContacts(extended with an optionalcontactInboxparam to preserve the worker's precise per-inbox ads-conversion enqueue), and a newcontactSequenceService.enrollFromFlow.subscribeBroadcast/unsubscribeBroadcastintentionally keep their directdbwrites — swapping them would have silently dropped an idempotency guard and an unsubscribe event..agents/rules/data-access.mdand 8 skills (business-data-access,feature-scaffold,orpc-api,integration-channel,security-review,testing-workflow,chatbotx-basecode,contact-filter) plusAGENTS.md/CLAUDE.md/docs/developer/workspace-api-tokens.mdto document the layering contract; ranpnpm sync:agent-instructions.contact-list,contact-repository-list-where,message-list-for-conversation,contact-resolve-id-by-identifier,conversation-resolve-contact-inbox-for-send, plus trigger/webhook/flow/automated-response/tag/sequence coverage).Test plan
pnpm lintpnpm --filter @chatbotx.io/database check-types && testpnpm --filter @chatbotx.io/business check-types && testpnpm --filter builder check-types && test(422 files / 2684 tests green)pnpm --filter worker check-types && test(199 files green; 1 pre-existing unrelated failure confirmed on base branch)invariant-guardagent dispatched on the final diff — no violations foundchatbotx-mcp-server—contacts_refreshProfile,contacts_createNote→contacts_listNotesimmediately after,contacts_createwith a mismatched inbox channel (expect 400, not 500)