feat(automation): implement public API for flows, triggers, keywords, ai-agents, reflinks, ai-triggers - #1102
Merged
Conversation
realcodesiman
force-pushed
the
refactor/data-access-builder-automation
branch
from
September 8, 2026 05:26
a86a842 to
33dfb4b
Compare
realcodesiman
force-pushed
the
refactor/data-access-builder-automation
branch
from
September 8, 2026 05:31
33dfb4b to
2421032
Compare
realcodesiman
added a commit
that referenced
this pull request
Sep 8, 2026
…ublic 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.
realcodesiman
added a commit
that referenced
this pull request
Sep 8, 2026
…ublic 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.
realcodesiman
force-pushed
the
refactor/data-access-builder-automation
branch
from
September 8, 2026 23:36
4641a45 to
7956296
Compare
…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)
…s, 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.
…ublic 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.
…on 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.
…I 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.
…dant 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
…siness 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.
…eflink 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.
realcodesiman
force-pushed
the
refactor/data-access-builder-automation
branch
from
September 9, 2026 09:39
2b9e06b to
0897a9f
Compare
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
.agents/rules/data-access.md.automationworkspace-token scope from read-only to full CRUD across flows (incl. publish/draft/versions/duplicate/import), triggers (realconditions/actionsinstead of hardcoded[]), keywords, ai-agents, reflinks, and a brand-new ai-triggers public router — 31 new MCP-exposed operations.Changes
reflink,ai-trigger) and services (flowService.update/list/findById,flowVersionService.updateDraft+updateDraftByFlowId,sequenceService.update,broadcastService.update,savedReplyService.create/update,botFieldServiceunique-violation handling,reflinkService.list/findOrFail/create/update, newaiTriggerService); all touched actions reduced to thin parse -> service -> return adapters with no directdbimport.apps/builder/src/features/{flows,triggers,automated-response,ai-agents,reflinks,ai-triggers}/api/public.tsgain the new routes;ai-triggersis a new router registered inapps/builder/src/routers/public.ts; each route uses an explicit.pick()output schema and documents 404/422 per operation.*-public-api.test.tsasserting each handler delegates to the correct service method; new service/repository unit tests forai-triggerandreflink; scope-enforcement test proving a non-automationtoken gets 403 on a new write route; updatedpublic-spec-operations.test.ts.snap(31 new operationIds, none renamed/removed).docs/developer/workspace-api-tokens.mdgains an automation endpoint-to-scope table.Review fixes (follow-up commits)
Three rounds of self-review on the surface above, kept as separate commits for reviewability:
type: "inbound"throughfindOrFail,update,setStatusanddeleteMany.AutomatedResponseserves twoFolderTypes from one table, soworkspaceId + idalone let an outbound (Page) response be read, mutated or deleted via the inbound Keywords endpoints.PUT /v1/flows/{id}/draftwas passing a flow id where a flow-version id was expected — fixed by the newflowVersionService.updateDraftByFlowId. Paginated triggers gained a deterministicorderByso pages cannot repeat or skip rows.aiTrigger.createnow writesquestions: []explicitly (the column has no DB default despite the drizzle.default()).ai-agent/ai-trigger/reflinkshort-circuit instead of issuing an emptySETand a spurious audit entry;flowService.list's return type corrected to thelimit/offsetthatparsePaginationactually spreads.Test plan
pnpm lintpnpm --filter builder check-types && testpnpm --filter @chatbotx.io/business check-types && testpnpm --filter @chatbotx.io/database check-types && testpnpm check:circularautomation;POST /v1/flowsthenPOST /v1/flows/{id}/publish;curl /v1/triggersreturns realconditions/actions;curl /v1/contactswith the same token returns 403; connect an MCP client and confirm the newflows_create,triggers_create,keywords_create,ai_agents_create,ref_links_create,ai_triggers_createtools appear