Skip to content

Commit 1ed43a4

Browse files
feat(automation): implement public API for flows, triggers, keywords, 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.
1 parent f8b7e0b commit 1ed43a4

171 files changed

Lines changed: 11500 additions & 5885 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,61 @@
22

33
exports[`public API spec — operation naming guard > operation list (operationId, method, path) matches the committed snapshot 1`] = `
44
[
5+
{
6+
"method": "POST",
7+
"operationId": "aiAgents.create",
8+
"path": "/v1/ai-agents",
9+
},
10+
{
11+
"method": "DELETE",
12+
"operationId": "aiAgents.delete",
13+
"path": "/v1/ai-agents/{id}",
14+
},
15+
{
16+
"method": "GET",
17+
"operationId": "aiAgents.get",
18+
"path": "/v1/ai-agents/{id}",
19+
},
520
{
621
"method": "GET",
722
"operationId": "aiAgents.list",
823
"path": "/v1/ai-agents",
924
},
25+
{
26+
"method": "PUT",
27+
"operationId": "aiAgents.update",
28+
"path": "/v1/ai-agents/{id}",
29+
},
30+
{
31+
"method": "POST",
32+
"operationId": "aiTriggers.create",
33+
"path": "/v1/ai-triggers",
34+
},
35+
{
36+
"method": "DELETE",
37+
"operationId": "aiTriggers.delete",
38+
"path": "/v1/ai-triggers/{id}",
39+
},
40+
{
41+
"method": "POST",
42+
"operationId": "aiTriggers.duplicate",
43+
"path": "/v1/ai-triggers/{id}/duplicate",
44+
},
45+
{
46+
"method": "GET",
47+
"operationId": "aiTriggers.get",
48+
"path": "/v1/ai-triggers/{id}",
49+
},
50+
{
51+
"method": "GET",
52+
"operationId": "aiTriggers.list",
53+
"path": "/v1/ai-triggers",
54+
},
55+
{
56+
"method": "PUT",
57+
"operationId": "aiTriggers.update",
58+
"path": "/v1/ai-triggers/{id}",
59+
},
1060
{
1161
"method": "PUT",
1262
"operationId": "botFields.bulkUpdate",
@@ -412,11 +462,56 @@ exports[`public API spec — operation naming guard > operation list (operationI
412462
"operationId": "externalWebhooks.list",
413463
"path": "/v1/external-webhooks",
414464
},
465+
{
466+
"method": "POST",
467+
"operationId": "flows.create",
468+
"path": "/v1/flows",
469+
},
470+
{
471+
"method": "DELETE",
472+
"operationId": "flows.delete",
473+
"path": "/v1/flows/{id}",
474+
},
475+
{
476+
"method": "POST",
477+
"operationId": "flows.duplicate",
478+
"path": "/v1/flows/{id}/duplicate",
479+
},
480+
{
481+
"method": "GET",
482+
"operationId": "flows.get",
483+
"path": "/v1/flows/{id}",
484+
},
485+
{
486+
"method": "POST",
487+
"operationId": "flows.import",
488+
"path": "/v1/flows/import",
489+
},
415490
{
416491
"method": "GET",
417492
"operationId": "flows.list",
418493
"path": "/v1/flows",
419494
},
495+
{
496+
"method": "POST",
497+
"operationId": "flows.publish",
498+
"path": "/v1/flows/{id}/publish",
499+
},
500+
{
501+
"method": "PATCH",
502+
"operationId": "flows.update",
503+
"path": "/v1/flows/{id}",
504+
},
505+
{
506+
"method": "PUT",
507+
"operationId": "flows.updateDraft",
508+
"path": "/v1/flows/{id}/draft",
509+
},
510+
{
511+
"method": "GET",
512+
"operationId": "flows.versions",
513+
"path": "/v1/flows/{id}/versions",
514+
},
420515
{
421516
"method": "POST",
422517
"operationId": "folders.create",
@@ -482,11 +577,36 @@ exports[`public API spec — operation naming guard > operation list (operationI
482577
"operationId": "integrations.tokenErrors",
483578
"path": "/v1/integrations/status/token-errors",
484579
},
580+
{
581+
"method": "POST",
582+
"operationId": "keywords.create",
583+
"path": "/v1/keywords",
584+
},
585+
{
586+
"method": "DELETE",
587+
"operationId": "keywords.delete",
588+
"path": "/v1/keywords/{id}",
589+
},
590+
{
591+
"method": "GET",
592+
"operationId": "keywords.get",
593+
"path": "/v1/keywords/{id}",
594+
},
485595
{
486596
"method": "GET",
487597
"operationId": "keywords.list",
488598
"path": "/v1/keywords",
489599
},
600+
{
601+
"method": "PUT",
602+
"operationId": "keywords.update",
603+
"path": "/v1/keywords/{id}",
604+
},
605+
{
606+
"method": "PATCH",
607+
"operationId": "keywords.updateStatus",
608+
"path": "/v1/keywords/{id}/status",
609+
},
490610
{
491611
"method": "POST",
492612
"operationId": "productCategories.create",
@@ -532,11 +652,31 @@ exports[`public API spec — operation naming guard > operation list (operationI
532652
"operationId": "products.update",
533653
"path": "/v1/products/{id}",
534654
},
655+
{
656+
"method": "POST",
657+
"operationId": "reflinks.create",
658+
"path": "/v1/ref-links",
659+
},
660+
{
661+
"method": "DELETE",
662+
"operationId": "reflinks.delete",
663+
"path": "/v1/ref-links/{id}",
664+
},
535665
{
536666
"method": "GET",
537667
"operationId": "reflinks.get",
538668
"path": "/v1/ref-links/{id}",
539669
},
670+
{
671+
"method": "GET",
672+
"operationId": "reflinks.list",
673+
"path": "/v1/ref-links",
674+
},
675+
{
676+
"method": "PUT",
677+
"operationId": "reflinks.update",
678+
"path": "/v1/ref-links/{id}",
679+
},
540680
{
541681
"method": "GET",
542682
"operationId": "savedReplies.list",
@@ -582,11 +722,36 @@ exports[`public API spec — operation naming guard > operation list (operationI
582722
"operationId": "templateMessages.list",
583723
"path": "/v1/template-messages",
584724
},
725+
{
726+
"method": "POST",
727+
"operationId": "triggers.create",
728+
"path": "/v1/triggers",
729+
},
730+
{
731+
"method": "DELETE",
732+
"operationId": "triggers.delete",
733+
"path": "/v1/triggers/{id}",
734+
},
735+
{
736+
"method": "GET",
737+
"operationId": "triggers.get",
738+
"path": "/v1/triggers/{id}",
739+
},
585740
{
586741
"method": "GET",
587742
"operationId": "triggers.list",
588743
"path": "/v1/triggers",
589744
},
745+
{
746+
"method": "PUT",
747+
"operationId": "triggers.update",
748+
"path": "/v1/triggers/{id}",
749+
},
750+
{
751+
"method": "PATCH",
752+
"operationId": "triggers.updateSettings",
753+
"path": "/v1/triggers/{id}/settings",
754+
},
590755
{
591756
"method": "POST",
592757
"operationId": "webhooks.create",

0 commit comments

Comments
 (0)