Skip to content

feat(automation): implement public API for flows, triggers, keywords, ai-agents, reflinks, ai-triggers - #1102

Merged
realcodesiman merged 8 commits into
mainfrom
refactor/data-access-builder-automation
Sep 9, 2026
Merged

feat(automation): implement public API for flows, triggers, keywords, ai-agents, reflinks, ai-triggers#1102
realcodesiman merged 8 commits into
mainfrom
refactor/data-access-builder-automation

Conversation

@realcodesiman

@realcodesiman realcodesiman commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Finishes the data-access chain (action -> service -> repository -> DB) for flows, sequences, broadcasts, saved-replies, reflinks, bot-fields, and greenfield ai-triggers, per .agents/rules/data-access.md.
  • Widens the automation workspace-token scope from read-only to full CRUD across flows (incl. publish/draft/versions/duplicate/import), triggers (real conditions/actions instead of hardcoded []), keywords, ai-agents, reflinks, and a brand-new ai-triggers public router — 31 new MCP-exposed operations.
  • Every new public handler calls the same service method its UI action already uses, so behavior can't drift between the two entry points.

Changes

  • Data-access layering: new/extended repositories (reflink, ai-trigger) and services (flowService.update/list/findById, flowVersionService.updateDraft + updateDraftByFlowId, sequenceService.update, broadcastService.update, savedReplyService.create/update, botFieldService unique-violation handling, reflinkService.list/findOrFail/create/update, new aiTriggerService); all touched actions reduced to thin parse -> service -> return adapters with no direct db import.
  • Public API: apps/builder/src/features/{flows,triggers,automated-response,ai-agents,reflinks,ai-triggers}/api/public.ts gain the new routes; ai-triggers is a new router registered in apps/builder/src/routers/public.ts; each route uses an explicit .pick() output schema and documents 404/422 per operation.
  • Tests: new per-feature *-public-api.test.ts asserting each handler delegates to the correct service method; new service/repository unit tests for ai-trigger and reflink; scope-enforcement test proving a non-automation token gets 403 on a new write route; updated public-spec-operations.test.ts.snap (31 new operationIds, none renamed/removed).
  • Docs: docs/developer/workspace-api-tokens.md gains 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:

  • Tenant/type scoping: the Keywords routes now thread type: "inbound" through findOrFail, update, setStatus and deleteMany. AutomatedResponse serves two FolderTypes from one table, so workspaceId + id alone let an outbound (Page) response be read, mutated or deleted via the inbound Keywords endpoints.
  • Data loss / correctness: PUT /v1/flows/{id}/draft was passing a flow id where a flow-version id was expected — fixed by the new flowVersionService.updateDraftByFlowId. Paginated triggers gained a deterministic orderBy so pages cannot repeat or skip rows. aiTrigger.create now writes questions: [] explicitly (the column has no DB default despite the drizzle .default()).
  • Contract fidelity: restored the flows list response contract and field-level validation errors; no-op updates in ai-agent/ai-trigger/reflink short-circuit instead of issuing an empty SET and a spurious audit entry; flowService.list's return type corrected to the limit/offset that parsePagination actually spreads.

Test plan

  • pnpm lint
  • pnpm --filter builder check-types && test
  • pnpm --filter @chatbotx.io/business check-types && test
  • pnpm --filter @chatbotx.io/database check-types && test
  • pnpm check:circular
  • Manual: create a workspace token scoped only to automation; POST /v1/flows then POST /v1/flows/{id}/publish; curl /v1/triggers returns real conditions/actions; curl /v1/contacts with the same token returns 403; connect an MCP client and confirm the new flows_create, triggers_create, keywords_create, ai_agents_create, ref_links_create, ai_triggers_create tools appear

@github-actions github-actions Bot added the improvement Refactor or performance improvement label Sep 6, 2026
@realcodesiman
realcodesiman force-pushed the refactor/data-access-builder-automation branch from a86a842 to 33dfb4b Compare September 8, 2026 05:26
@realcodesiman realcodesiman changed the title refactor(builder): move flows, triggers, sequences, broadcasts, webhooks data access into business feat(automation): implement public API for flows, triggers, keywords, ai-agents, reflinks, ai-triggers Sep 8, 2026
@github-actions github-actions Bot added the feature New feature or request label Sep 8, 2026
@realcodesiman
realcodesiman force-pushed the refactor/data-access-builder-automation branch from 33dfb4b to 2421032 Compare September 8, 2026 05:31
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
realcodesiman force-pushed the refactor/data-access-builder-automation branch from 4641a45 to 7956296 Compare September 8, 2026 23:36
…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
realcodesiman force-pushed the refactor/data-access-builder-automation branch from 2b9e06b to 0897a9f Compare September 9, 2026 09:39
@realcodesiman
realcodesiman merged commit 1ed43a4 into main Sep 9, 2026
4 checks passed
@realcodesiman
realcodesiman deleted the refactor/data-access-builder-automation branch September 9, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request improvement Refactor or performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant